-
Notifications
You must be signed in to change notification settings - Fork 293
Expand file tree
/
Copy pathtags.py
More file actions
929 lines (762 loc) · 33.4 KB
/
tags.py
File metadata and controls
929 lines (762 loc) · 33.4 KB
Edit and raw actions
OlderNewer
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.
4
5
from __future__ import annotations
6
7
import logging
8
import operator
9
import platform
10
import re
11
import struct
12
import subprocess
13
import sys
14
import sysconfig
15
from collections.abc import Iterable, Iterator, Sequence
16
from importlib.machinery import EXTENSION_SUFFIXES
17
from typing import (
18
TYPE_CHECKING,
19
TypeVar,
20
cast,
21
)
22
23
from . import _manylinux, _musllinux
24
25
if TYPE_CHECKING:
26
from collections.abc import Callable
27
from collections.abc import Set as AbstractSet
28
29
30
__all__ = [
31
"INTERPRETER_SHORT_NAMES",
32
"AppleVersion",
33
"PythonVersion",
34
"Tag",
35
"UnsortedTagsError",
36
"android_platforms",
37
"compatible_tags",
38
"cpython_tags",
39
"create_compatible_tags_selector",
40
"generic_tags",
41
"interpreter_name",
42
"interpreter_version",
43
"ios_platforms",
44
"mac_platforms",
45
"parse_tag",
46
"platform_tags",
47
"sys_tags",
48
]
49
50
51
def __dir__() -> list[str]:
52
return __all__
53
54
55
logger = logging.getLogger(__name__)
56
57
PythonVersion = Sequence[int]
58
AppleVersion = tuple[int, int]
59
_T = TypeVar("_T")
60
61
INTERPRETER_SHORT_NAMES: dict[str, str] = {
62
"python": "py", # Generic.
63
"cpython": "cp",
64
"pypy": "pp",
65
"ironpython": "ip",
66
"jython": "jy",
67
}
68
69
70
# This function can be unit tested without reloading the module
71
# (Unlike _32_BIT_INTERPRETER)
72
def _compute_32_bit_interpreter() -> bool:
73
return struct.calcsize("P") == 4
74
75
76
_32_BIT_INTERPRETER = _compute_32_bit_interpreter()
77
78
79
class UnsortedTagsError(ValueError):
80
"""
81
Raised when a tag component is not in sorted order per PEP 425.
82
"""
83
84
85
class Tag:
86
"""
87
A representation of the tag triple for a wheel.
88
89
Instances are considered immutable and thus are hashable. Equality checking
90
is also supported.
91
92
Instances are safe to serialize with :mod:`pickle`. They use a stable
93
format so the same pickle can be loaded in future packaging releases.
94
95
.. versionchanged:: 26.2
96
97
Added a stable pickle format. Pickles created with packaging 26.2+ can
98
be unpickled with future releases. Backward compatibility with pickles
99
from packaging < 26.2 is supported but may be removed in a future
100
release.
101
"""
102
103
__slots__ = ["_abi", "_hash", "_interpreter", "_platform"]
104
105
def __init__(self, interpreter: str, abi: str, platform: str) -> None:
106
"""
107
:param str interpreter: The interpreter name, e.g. ``"py"``
108
(see :attr:`INTERPRETER_SHORT_NAMES` for mapping
109
well-known interpreter names to their short names).
110
:param str abi: The ABI that a wheel supports, e.g. ``"cp37m"``.
111
:param str platform: The OS/platform the wheel supports,
112
e.g. ``"win_amd64"``.
113
"""
114
self._interpreter = interpreter.lower()
115
self._abi = abi.lower()
116
self._platform = platform.lower()
117
# The __hash__ of every single element in a Set[Tag] will be evaluated each time
118
# that a set calls its `.disjoint()` method, which may be called hundreds of
119
# times when scanning a page of links for packages with tags matching that
120
# Set[Tag]. Pre-computing the value here produces significant speedups for
121
# downstream consumers.
122
self._hash = hash((self._interpreter, self._abi, self._platform))
123
124
@property
125
def interpreter(self) -> str:
126
"""
127
The interpreter name, e.g. ``"py"`` (see
128
:attr:`INTERPRETER_SHORT_NAMES` for mapping well-known interpreter
129
names to their short names).
130
"""
131
return self._interpreter
132
133
@property
134
def abi(self) -> str:
135
"""
136
The supported ABI.
137
"""
138
return self._abi
139
140
@property
141
def platform(self) -> str:
142
"""
143
The OS/platform.
144
"""
145
return self._platform
146
147
def __eq__(self, other: object) -> bool:
148
if not isinstance(other, Tag):
149
return NotImplemented
150
151
return (
152
(self._hash == other._hash) # Short-circuit ASAP for perf reasons.
153
and (self._platform == other._platform)
154
and (self._abi == other._abi)
155
and (self._interpreter == other._interpreter)
156
)
157
158
def __hash__(self) -> int:
159
return self._hash
160
161
def __str__(self) -> str:
162
return f"{self._interpreter}-{self._abi}-{self._platform}"
163
164
def __repr__(self) -> str:
165
return f"<{self} @ {id(self)}>"
166
167
def __getstate__(self) -> tuple[str, str, str]:
168
# Return state as a 3-item tuple: (interpreter, abi, platform).
169
# Cache member _hash is excluded and will be recomputed.
170
return (self._interpreter, self._abi, self._platform)
171
172
def __setstate__(self, state: object) -> None:
173
if isinstance(state, tuple):
174
if len(state) == 3 and all(isinstance(s, str) for s in state):
175
# New format (26.2+): (interpreter, abi, platform)
176
self._interpreter, self._abi, self._platform = state
177
self._hash = hash((self._interpreter, self._abi, self._platform))
178
return
179
if len(state) == 2 and isinstance(state[1], dict):
180
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
181
_, slots = state
182
try:
183
interpreter = slots["_interpreter"]
184
abi = slots["_abi"]
185
platform = slots["_platform"]
186
except KeyError:
187
raise TypeError(f"Cannot restore Tag from {state!r}") from None
188
if not all(
189
isinstance(value, str) for value in (interpreter, abi, platform)
190
):
191
raise TypeError(f"Cannot restore Tag from {state!r}")
192
self._interpreter = interpreter.lower()
193
self._abi = abi.lower()
194
self._platform = platform.lower()
195
self._hash = hash((self._interpreter, self._abi, self._platform))
196
return
197
raise TypeError(f"Cannot restore Tag from {state!r}")
198
199
200
def parse_tag(tag: str, *, validate_order: bool = False) -> frozenset[Tag]:
201
"""
202
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of
203
:class:`Tag` instances.
204
205
Returning a set is required due to the possibility that the tag is a
206
`compressed tag set`_, e.g. ``"py2.py3-none-any"`` which supports both
207
Python 2 and Python 3.
208
209
If **validate_order** is true, compressed tag set components are checked
210
to be in sorted order as required by PEP 425.
211
212
:param str tag: The tag to parse, e.g. ``"py3-none-any"``.
213
:param bool validate_order: Check whether compressed tag set components
214
are in sorted order.
215
:raises UnsortedTagsError: If **validate_order** is true and any compressed tag
216
set component is not in sorted order.
217
218
.. versionadded:: 26.1
219
The *validate_order* parameter.
220
"""
221
tags = set()
222
interpreters, abis, platforms = tag.split("-")
223
if validate_order:
224
for component in (interpreters, abis, platforms):
225
parts = component.split(".")
226
if parts != sorted(parts):
227
raise UnsortedTagsError(
228
f"Tag component {component!r} is not in sorted order per PEP 425"
229
)
230
for interpreter in interpreters.split("."):
231
for abi in abis.split("."):
232
for platform_ in platforms.split("."):
233
tags.add(Tag(interpreter, abi, platform_))
234
return frozenset(tags)
235
236
237
def _get_config_var(name: str, warn: bool = False) -> int | str | None:
238
value: int | str | None = sysconfig.get_config_var(name)
239
if value is None and warn:
240
logger.debug(
241
"Config variable '%s' is unset, Python ABI tag may be incorrect", name
242
)
243
return value
244
245
246
def _normalize_string(string: str) -> str:
247
return string.replace(".", "_").replace("-", "_").replace(" ", "_")
248
249
250
def _is_threaded_cpython(abis: list[str]) -> bool:
251
"""
252
Determine if the ABI corresponds to a threaded (`--disable-gil`) build.
253
254
The threaded builds are indicated by a "t" in the abiflags.
255
"""
256
if len(abis) == 0:
257
return False
258
# expect e.g., cp313
259
m = re.match(r"cp\d+(.*)", abis[0])
260
if not m:
261
return False
262
abiflags = m.group(1)
263
return "t" in abiflags
264
265
266
def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool:
267
"""
268
Determine if the Python version supports abi3.
269
270
PEP 384 was first implemented in Python 3.2. The free-threaded
271
builds do not support abi3.
272
"""
273
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
274
275
276
def _abi3t_applies(python_version: PythonVersion, threading: bool) -> bool:
277
"""
278
Determine if the Python version supports abi3t.
279
280
PEP 803 was first implemented in Python 3.15 but, per PEP 803, this
281
returns tags going back to Python 3.2 to mirror the abi3
282
implementation and leave open the possibility of abi3t wheels
283
supporting older Python versions.
284
285
"""
286
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and threading
287
288
289
def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]:
290
py_version = tuple(py_version) # To allow for version comparison.
291
abis = []
292
version = _version_nodot(py_version[:2])
293
threading = debug = pymalloc = ucs4 = ""
294
with_debug = _get_config_var("Py_DEBUG", warn)
295
has_refcount = hasattr(sys, "gettotalrefcount")
296
# Windows doesn't set Py_DEBUG, so checking for support of debug-compiled
297
# extension modules is the best option.
298
# https://github.com/pypa/pip/issues/3383#issuecomment-173267692
299
has_ext = "_d.pyd" in EXTENSION_SUFFIXES
300
if with_debug or (with_debug is None and (has_refcount or has_ext)):
301
debug = "d"
302
if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn):
303
threading = "t"
304
if py_version < (3, 8):
305
with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
306
if with_pymalloc or with_pymalloc is None:
307
pymalloc = "m"
308
if py_version < (3, 3):
309
unicode_size = _get_config_var("Py_UNICODE_SIZE", warn)
310
if unicode_size == 4 or (
311
unicode_size is None and sys.maxunicode == 0x10FFFF
312
):
313
ucs4 = "u"
314
elif debug:
315
# Debug builds can also load "normal" extension modules.
316
# We can also assume no UCS-4 or pymalloc requirement.
317
abis.append(f"cp{version}{threading}")
318
abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}")
319
return abis
320
321
322
def cpython_tags(
323
python_version: PythonVersion | None = None,
324
abis: Iterable[str] | None = None,
325
platforms: Iterable[str] | None = None,
326
*,
327
warn: bool = False,
328
) -> Iterator[Tag]:
329
"""
330
Yields the tags for the CPython interpreter.
331
332
The specific tags generated are:
333
334
- ``cp<python_version>-<abi>-<platform>``
335
- ``cp<python_version>-<stable_abi>-<platform>``
336
- ``cp<python_version>-none-<platform>``
337
- ``cp<older version>-<stable_abi>-<platform>`` where "older version" is all older
338
minor versions down to Python 3.2 (when ``abi3`` was introduced)
339
340
If ``python_version`` only provides a major-only version then only
341
user-provided ABIs via ``abis`` and the ``none`` ABI will be used.
342
343
The ``stable_abi`` will be either ``abi3`` or ``abi3t`` if `abi` is a
344
GIL-enabled ABI like `"cp315"` or a free-threaded ABI like `"cp315t"`,
345
respectively.
346
347
:param Sequence python_version: A one- or two-item sequence representing the
348
targeted Python version. Defaults to
349
``sys.version_info[:2]``.
350
:param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
351
compatible with the current system.
352
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
353
platforms compatible with the current system.
354
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
355
"""
356
if not python_version:
357
python_version = sys.version_info[:2]
358
359
interpreter = f"cp{_version_nodot(python_version[:2])}"
360
361
if abis is None:
362
abis = _cpython_abis(python_version, warn) if len(python_version) > 1 else []
363
abis = list(abis)
364
# 'abi3' and 'none' are explicitly handled later.
365
for explicit_abi in ("abi3", "none"):
366
try:
367
abis.remove(explicit_abi)
368
except ValueError: # noqa: PERF203
369
pass
370
371
platforms = list(platforms or platform_tags())
372
for abi in abis:
373
for platform_ in platforms:
374
yield Tag(interpreter, abi, platform_)
375
376
threading = _is_threaded_cpython(abis)
377
use_abi3 = _abi3_applies(python_version, threading)
378
use_abi3t = _abi3t_applies(python_version, threading)
379
380
if use_abi3:
381
yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
382
if use_abi3t:
383
yield from (Tag(interpreter, "abi3t", platform_) for platform_ in platforms)
384
385
yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
386
387
if use_abi3 or use_abi3t:
388
for minor_version in range(python_version[1] - 1, 1, -1):
389
for platform_ in platforms:
390
version = _version_nodot((python_version[0], minor_version))
391
interpreter = f"cp{version}"
392
if use_abi3:
393
yield Tag(interpreter, "abi3", platform_)
394
if use_abi3t:
395
# Support for abi3t was introduced in Python 3.15, but in
396
# principle abi3t wheels are possible for older limited API
397
# versions, so allow things like ("cp37", "abi3t", "platform")
398
yield Tag(interpreter, "abi3t", platform_)
399
400
401
def _generic_abi() -> list[str]:
402
"""
403
Return the ABI tag based on EXT_SUFFIX.
404
"""
405
# The following are examples of `EXT_SUFFIX`.
406
# We want to keep the parts which are related to the ABI and remove the
407
# parts which are related to the platform:
408
# - linux: '.cpython-310-x86_64-linux-gnu.so' => cp310
409
# - mac: '.cpython-310-darwin.so' => cp310
410
# - win: '.cp310-win_amd64.pyd' => cp310
411
# - win: '.pyd' => cp37 (uses _cpython_abis())
412
# - pypy: '.pypy38-pp73-x86_64-linux-gnu.so' => pypy38_pp73
413
# - graalpy: '.graalpy-38-native-x86_64-darwin.dylib'
414
# => graalpy_38_native
415
416
ext_suffix = _get_config_var("EXT_SUFFIX", warn=True)
417
if not isinstance(ext_suffix, str) or ext_suffix[0] != ".":
418
raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')")
419
parts = ext_suffix.split(".")
420
if len(parts) < 3:
421
# CPython3.7 and earlier uses ".pyd" on Windows.
422
return _cpython_abis(sys.version_info[:2])
423
soabi = parts[1]
424
if soabi.startswith("cpython"):
425
# non-windows
426
abi = "cp" + soabi.split("-")[1]
427
elif soabi.startswith("cp"):
428
# windows
429
abi = soabi.split("-")[0]
430
elif soabi.startswith("pypy"):
431
abi = "-".join(soabi.split("-")[:2])
432
elif soabi.startswith("graalpy"):
433
abi = "-".join(soabi.split("-")[:3])
434
elif soabi:
435
# pyston, ironpython, others?
436
abi = soabi
437
else:
438
return []
439
return [_normalize_string(abi)]
440
441
442
def generic_tags(
443
interpreter: str | None = None,
444
abis: Iterable[str] | None = None,
445
platforms: Iterable[str] | None = None,
446
*,
447
warn: bool = False,
448
) -> Iterator[Tag]:
449
"""
450
Yields the tags for an interpreter which requires no specialization.
451
452
This function should be used if one of the other interpreter-specific
453
functions provided by this module is not appropriate (i.e. not calculating
454
tags for a CPython interpreter).
455
456
The specific tags generated are:
457
458
- ``<interpreter>-<abi>-<platform>``
459
460
The ``"none"`` ABI will be added if it was not explicitly provided.
461
462
:param str interpreter: The name of the interpreter. Defaults to being
463
calculated.
464
:param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
465
compatible with the current system.
466
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
467
platforms compatible with the current system.
468
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
469
"""
470
if not interpreter:
471
interp_name = interpreter_name()
472
interp_version = interpreter_version(warn=warn)
473
interpreter = f"{interp_name}{interp_version}"
474
abis = _generic_abi() if abis is None else list(abis)
475
platforms = list(platforms or platform_tags())
476
if "none" not in abis:
477
abis.append("none")
478
for abi in abis:
479
for platform_ in platforms:
480
yield Tag(interpreter, abi, platform_)
481
482
483
def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
484
"""
485
Yields Python versions in descending order.
486
487
After the latest version, the major-only version will be yielded, and then
488
all previous versions of that major version.
489
"""
490
if len(py_version) > 1:
491
yield f"py{_version_nodot(py_version[:2])}"
492
yield f"py{py_version[0]}"
493
if len(py_version) > 1:
494
for minor in range(py_version[1] - 1, -1, -1):
495
yield f"py{_version_nodot((py_version[0], minor))}"
496
497
498
def compatible_tags(
499
python_version: PythonVersion | None = None,
500
interpreter: str | None = None,
501
platforms: Iterable[str] | None = None,
502
) -> Iterator[Tag]:
503
"""
504
Yields the tags for an interpreter compatible with the Python version
505
specified by ``python_version``.
506
507
The specific tags generated are:
508
509
- ``py*-none-<platform>``
510
- ``<interpreter>-none-any`` if ``interpreter`` is provided
511
- ``py*-none-any``
512
513
:param Sequence python_version: A one- or two-item sequence representing the
514
compatible version of Python. Defaults to
515
``sys.version_info[:2]``.
516
:param str interpreter: The name of the interpreter (if known), e.g.
517
``"cp38"``. Defaults to the current interpreter.
518
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
519
platforms compatible with the current system.
520
"""
521
if not python_version:
522
python_version = sys.version_info[:2]
523
platforms = list(platforms or platform_tags())
524
for version in _py_interpreter_range(python_version):
525
for platform_ in platforms:
526
yield Tag(version, "none", platform_)
527
if interpreter:
528
yield Tag(interpreter, "none", "any")
529
for version in _py_interpreter_range(python_version):
530
yield Tag(version, "none", "any")
531
532
533
def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
534
if not is_32bit:
535
return arch
536
537
if arch.startswith("ppc"):
538
return "ppc"
539
540
return "i386"
541
542
543
def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:
544
formats = [cpu_arch]
545
if cpu_arch == "x86_64":
546
if version < (10, 4):
547
return []
548
formats.extend(["intel", "fat64", "fat32"])
549
550
elif cpu_arch == "i386":
551
if version < (10, 4):
552
return []
553
formats.extend(["intel", "fat32", "fat"])
554
555
elif cpu_arch == "ppc64":
556
# TODO: Need to care about 32-bit PPC for ppc64 through 10.2?
557
if version > (10, 5) or version < (10, 4):
558
return []
559
formats.append("fat64")
560
561
elif cpu_arch == "ppc":
562
if version > (10, 6):
563
return []
564
formats.extend(["fat32", "fat"])
565
566
if cpu_arch in {"arm64", "x86_64"}:
567
formats.append("universal2")
568
569
if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}:
570
formats.append("universal")
571
572
return formats
573
574
575
def mac_platforms(
576
version: AppleVersion | None = None, arch: str | None = None
577
) -> Iterator[str]:
578
"""
579
Yields the :attr:`~Tag.platform` tags for macOS.
580
581
The `version` parameter is a two-item tuple specifying the macOS version to
582
generate platform tags for. The `arch` parameter is the CPU architecture to
583
generate platform tags for. Both parameters default to the appropriate value
584
for the current system.
585
586
:param tuple version: A two-item tuple representing the version of macOS.
587
Defaults to the current system's version.
588
:param str arch: The CPU architecture. Defaults to the architecture of the
589
current system, e.g. ``"x86_64"``.
590
591
.. note::
592
Equivalent support for the other major platforms is purposefully not
593
provided:
594
595
- On Windows, platform compatibility is statically specified
596
- On Linux, code must be run on the system itself to determine
597
compatibility
598
"""
599
version_str, _, cpu_arch = platform.mac_ver()
600
if version is None:
601
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
602
if version == (10, 16):
603
# When built against an older macOS SDK, Python will report macOS 10.16
604
# instead of the real version.
605
version_str = subprocess.run(
606
[
607
sys.executable,
608
"-sS",
609
"-c",
610
"import platform; print(platform.mac_ver()[0])",
611
],
612
check=True,
613
env={"SYSTEM_VERSION_COMPAT": "0"},
614
stdout=subprocess.PIPE,
615
text=True,
616
).stdout
617
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
618
619
if arch is None:
620
arch = _mac_arch(cpu_arch)
621
622
if (10, 0) <= version < (11, 0):
623
# Prior to Mac OS 11, each yearly release of Mac OS bumped the
624
# "minor" version number. The major version was always 10.
625
major_version = 10
626
for minor_version in range(version[1], -1, -1):
627
compat_version = major_version, minor_version
628
binary_formats = _mac_binary_formats(compat_version, arch)
629
for binary_format in binary_formats:
630
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
631
632
if version >= (11, 0):
633
# Starting with Mac OS 11, each yearly release bumps the major version
634
# number. The minor versions are now the midyear updates.
635
minor_version = 0
636
for major_version in range(version[0], 10, -1):
637
compat_version = major_version, minor_version
638
binary_formats = _mac_binary_formats(compat_version, arch)
639
for binary_format in binary_formats:
640
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
641
642
if version >= (11, 0):
643
# Mac OS 11 on x86_64 is compatible with binaries from previous releases.
644
# Arm64 support was introduced in 11.0, so no Arm binaries from previous
645
# releases exist.
646
#
647
# However, the "universal2" binary format can have a
648
# macOS version earlier than 11.0 when the x86_64 part of the binary supports
649
# that version of macOS.
650
major_version = 10
651
if arch == "x86_64":
652
for minor_version in range(16, 3, -1):
653
compat_version = major_version, minor_version
654
binary_formats = _mac_binary_formats(compat_version, arch)
655
for binary_format in binary_formats:
656
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
657
else:
658
for minor_version in range(16, 3, -1):
659
compat_version = major_version, minor_version
660
binary_format = "universal2"
661
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
662
663
664
def ios_platforms(
665
version: AppleVersion | None = None, multiarch: str | None = None
666
) -> Iterator[str]:
667
"""
668
669
Yields the :attr:`~Tag.platform` tags for iOS.
670
671
:param tuple version: A two-item tuple representing the version of iOS.
672
Defaults to the current system's version.
673
:param str multiarch: The CPU architecture+ABI to be used. This should be in
674
the format by ``sys.implementation._multiarch`` (e.g.,
675
``arm64_iphoneos`` or ``x86_64_iphonesimulator``).
676
Defaults to the current system's multiarch value.
677
678
.. note::
679
Behavior of this method is undefined if invoked on non-iOS platforms
680
without providing explicit version and multiarch arguments.
681
"""
682
if version is None:
683
# if iOS is the current platform, ios_ver *must* be defined. However,
684
# it won't exist for CPython versions before 3.13, which causes a mypy
685
# error.
686
_, release, _, _ = platform.ios_ver() # type: ignore[attr-defined, unused-ignore]
687
version = cast("AppleVersion", tuple(map(int, release.split(".")[:2])))
688
689
if multiarch is None:
690
multiarch = sys.implementation._multiarch
691
multiarch = multiarch.replace("-", "_")
692
693
ios_platform_template = "ios_{major}_{minor}_{multiarch}"
694
695
# Consider any iOS major.minor version from the version requested, down to
696
# 12.0. 12.0 is the first iOS version that is known to have enough features
697
# to support CPython. Consider every possible minor release up to X.9. There
698
# highest the minor has ever gone is 8 (14.8 and 15.8) but having some extra
699
# candidates that won't ever match doesn't really hurt, and it saves us from
700
# having to keep an explicit list of known iOS versions in the code. Return
701
# the results descending order of version number.
702
703
# If the requested major version is less than 12, there won't be any matches.
704
if version[0] < 12:
705
return
706
707
# Consider the actual X.Y version that was requested.
708
yield ios_platform_template.format(
709
major=version[0], minor=version[1], multiarch=multiarch
710
)
711
712
# Consider every minor version from X.0 to the minor version prior to the
713
# version requested by the platform.
714
for minor in range(version[1] - 1, -1, -1):
715
yield ios_platform_template.format(
716
major=version[0], minor=minor, multiarch=multiarch
717
)
718
719
for major in range(version[0] - 1, 11, -1):
720
for minor in range(9, -1, -1):
721
yield ios_platform_template.format(
722
major=major, minor=minor, multiarch=multiarch
723
)
724
725
726
def android_platforms(
727
api_level: int | None = None, abi: str | None = None
728
) -> Iterator[str]:
729
"""
730
Yields the :attr:`~Tag.platform` tags for Android. If this function is invoked on
731
non-Android platforms, the ``api_level`` and ``abi`` arguments are required.
732
733
:param int api_level: The maximum `API level
734
<https://developer.android.com/tools/releases/platforms>`__ to return. Defaults
735
to the current system's version, as returned by ``platform.android_ver``.
736
:param str abi: The `Android ABI <https://developer.android.com/ndk/guides/abis>`__,
737
e.g. ``arm64_v8a``. Defaults to the current system's ABI , as returned by
738
``sysconfig.get_platform``. Hyphens and periods will be replaced with
739
underscores.
740
"""
741
if platform.system() != "Android" and (api_level is None or abi is None):
742
raise TypeError(
743
"on non-Android platforms, the api_level and abi arguments are required"
744
)
745
746
if api_level is None:
747
# Python 3.13 was the first version to return platform.system() == "Android",
748
# and also the first version to define platform.android_ver().
749
api_level = platform.android_ver().api_level # type: ignore[attr-defined]
750
751
if abi is None:
752
abi = sysconfig.get_platform().split("-")[-1]
753
abi = _normalize_string(abi)
754
755
# 16 is the minimum API level known to have enough features to support CPython
756
# without major patching. Yield every API level from the maximum down to the
757
# minimum, inclusive.
758
min_api_level = 16
759
for ver in range(api_level, min_api_level - 1, -1):
760
yield f"android_{ver}_{abi}"
761
762
763
def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
764
linux = _normalize_string(sysconfig.get_platform())
765
if not linux.startswith("linux_"):
766
# we should never be here, just yield the sysconfig one and return
767
yield linux
768
return
769
if is_32bit:
770
if linux == "linux_x86_64":
771
linux = "linux_i686"
772
elif linux == "linux_aarch64":
773
linux = "linux_armv8l"
774
_, arch = linux.split("_", 1)
775
archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch])
776
yield from _manylinux.platform_tags(archs)
777
yield from _musllinux.platform_tags(archs)
778
for arch in archs:
779
yield f"linux_{arch}"
780
781
782
def _emscripten_platforms() -> Iterator[str]:
783
pyemscripten_platform_version = sysconfig.get_config_var(
784
"PYEMSCRIPTEN_PLATFORM_VERSION"
785
)
786
if pyemscripten_platform_version:
787
yield f"pyemscripten_{pyemscripten_platform_version}_wasm32"
788
yield from _generic_platforms()
789
790
791
def _generic_platforms() -> Iterator[str]:
792
yield _normalize_string(sysconfig.get_platform())
793
794
795
def platform_tags() -> Iterator[str]:
796
"""
797
Yields the :attr:`~Tag.platform` tags for the running interpreter.
798
"""
799
if platform.system() == "Darwin":
800
return mac_platforms()
801
elif platform.system() == "iOS":
802
return ios_platforms()
803
elif platform.system() == "Android":
804
return android_platforms()
805
elif platform.system() == "Linux":
806
return _linux_platforms()
807
elif platform.system() == "Emscripten":
808
return _emscripten_platforms()
809
else:
810
return _generic_platforms()
811
812
813
def interpreter_name() -> str:
814
"""
815
Returns the name of the running interpreter.
816
817
Some implementations have a reserved, two-letter abbreviation which will
818
be returned when appropriate.
819
820
This typically acts as the prefix to the :attr:`~Tag.interpreter` tag.
821
"""
822
name = sys.implementation.name
823
return INTERPRETER_SHORT_NAMES.get(name) or name
824
825
826
def interpreter_version(*, warn: bool = False) -> str:
827
"""
828
Returns the running interpreter's version.
829
830
This typically acts as the suffix to the :attr:`~Tag.interpreter` tag.
831
832
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
833
"""
834
version = _get_config_var("py_version_nodot", warn=warn)
835
return str(version) if version else _version_nodot(sys.version_info[:2])
836
837
838
def _version_nodot(version: PythonVersion) -> str:
839
return "".join(map(str, version))
840
841
842
def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
843
"""
844
Yields the sequence of tag triples that the running interpreter supports.
845
846
The iterable is ordered so that the best-matching tag is first in the
847
sequence. The exact preferential order to tags is interpreter-specific, but
848
in general the tag importance is in the order of:
849
850
1. Interpreter
851
2. Platform
852
3. ABI
853
854
This order is due to the fact that an ABI is inherently tied to the
855
platform, but platform-specific code is not necessarily tied to the ABI. The
856
interpreter is the most important tag as it dictates basic support for any
857
wheel.
858
859
The function returns an iterable in order to allow for the possible
860
short-circuiting of tag generation if the entire sequence is not necessary
861
and tag calculation happens to be expensive.
862
863
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
864
865
.. versionchanged:: 21.3
866
Added the `pp3-none-any` tag (:issue:`311`).
867
.. versionchanged:: 27.0
868
Added the `abi3t` tag (:issue:`1099`).
869
"""
870
871
interp_name = interpreter_name()
872
if interp_name == "cp":
873
yield from cpython_tags(warn=warn)
874
else:
875
yield from generic_tags()
876
877
if interp_name == "pp":
878
interp = "pp3"
879
elif interp_name == "cp":
880
interp = "cp" + interpreter_version(warn=warn)
881
else:
882
interp = None
883
yield from compatible_tags(interpreter=interp)
884
885
886
def create_compatible_tags_selector(
887
tags: Iterable[Tag],
888
) -> Callable[[Iterable[tuple[_T, AbstractSet[Tag]]]], Iterator[_T]]:
889
"""Create a callable to select things compatible with supported tags.
890
891
This function accepts an ordered sequence of tags, with the preferred
892
tags first.
893
894
The returned callable accepts an iterable of tuples (thing, set[Tag]),
895
and returns an iterator of things, with the things with the best
896
matching tags first.
897
898
Example to select compatible wheel filenames:
899
900
>>> from packaging import tags
901
>>> from packaging.utils import parse_wheel_filename
902
>>> selector = tags.create_compatible_tags_selector(tags.sys_tags())
903
>>> filenames = ["foo-1.0-py3-none-any.whl", "foo-1.0-py2-none-any.whl"]
904
>>> list(selector([
905
... (filename, parse_wheel_filename(filename)[-1]) for filename in filenames
906
... ]))
907
['foo-1.0-py3-none-any.whl']
908
909
.. versionadded:: 26.1
910
"""
911
tag_ranks: dict[Tag, int] = {}
912
for rank, tag in enumerate(tags):
913
tag_ranks.setdefault(tag, rank) # ignore duplicate tags, keep first
914
supported_tags = tag_ranks.keys()
915
916
def selector(
917
tagged_things: Iterable[tuple[_T, AbstractSet[Tag]]],
918
) -> Iterator[_T]:
919
ranked_things: list[tuple[_T, int]] = []
920
for thing, thing_tags in tagged_things:
921
supported_thing_tags = thing_tags & supported_tags
922
if supported_thing_tags:
923
thing_rank = min(tag_ranks[t] for t in supported_thing_tags)
924
ranked_things.append((thing, thing_rank))
925
return iter(
926
thing for thing, _ in sorted(ranked_things, key=operator.itemgetter(1))
927
)
928
929
return selector