Skip to content

Latest commit

 

History

History
929 lines (762 loc) · 33.4 KB

File metadata and controls

929 lines (762 loc) · 33.4 KB
 
May 10, 2019
May 10, 2019
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
Mar 10, 2024
Mar 10, 2024
5
from __future__ import annotations
6
Oct 1, 2019
Oct 1, 2019
7
import logging
Mar 26, 2026
Mar 26, 2026
8
import operator
May 10, 2019
May 10, 2019
9
import platform
Oct 23, 2023
Oct 23, 2023
10
import re
Sep 1, 2023
Sep 1, 2023
11
import struct
Jun 17, 2022
Jun 17, 2022
12
import subprocess
May 10, 2019
May 10, 2019
13
import sys
14
import sysconfig
Apr 27, 2026
Apr 27, 2026
15
from collections.abc import Iterable, Iterator, Sequence
Feb 6, 2021
Feb 6, 2021
16
from importlib.machinery import EXTENSION_SUFFIXES
Feb 9, 2021
Feb 9, 2021
17
from typing import (
Mar 26, 2026
Mar 26, 2026
18
TYPE_CHECKING,
19
TypeVar,
Feb 9, 2021
Feb 9, 2021
20
cast,
21
)
May 10, 2019
May 10, 2019
22
Jul 1, 2021
Jul 1, 2021
23
from . import _manylinux, _musllinux
May 3, 2021
May 3, 2021
24
Mar 26, 2026
Mar 26, 2026
25
if TYPE_CHECKING:
Apr 27, 2026
Apr 27, 2026
26
from collections.abc import Callable
27
from collections.abc import Set as AbstractSet
Mar 26, 2026
Mar 26, 2026
28
29
Jan 23, 2026
Jan 23, 2026
30
__all__ = [
31
"INTERPRETER_SHORT_NAMES",
32
"AppleVersion",
33
"PythonVersion",
34
"Tag",
Apr 9, 2026
Apr 9, 2026
35
"UnsortedTagsError",
Jan 23, 2026
Jan 23, 2026
36
"android_platforms",
37
"compatible_tags",
38
"cpython_tags",
Mar 26, 2026
Mar 26, 2026
39
"create_compatible_tags_selector",
Jan 23, 2026
Jan 23, 2026
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
Oct 1, 2019
Oct 1, 2019
55
logger = logging.getLogger(__name__)
56
Feb 9, 2021
Feb 9, 2021
57
PythonVersion = Sequence[int]
Apr 27, 2026
Apr 27, 2026
58
AppleVersion = tuple[int, int]
Mar 26, 2026
Mar 26, 2026
59
_T = TypeVar("_T")
Feb 9, 2021
Feb 9, 2021
60
Mar 10, 2024
Mar 10, 2024
61
INTERPRETER_SHORT_NAMES: dict[str, str] = {
May 10, 2019
May 10, 2019
62
"python": "py", # Generic.
63
"cpython": "cp",
64
"pypy": "pp",
65
"ironpython": "ip",
66
"jython": "jy",
Feb 9, 2021
Feb 9, 2021
67
}
May 10, 2019
May 10, 2019
68
69
Apr 8, 2026
Apr 8, 2026
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()
May 10, 2019
May 10, 2019
77
78
Apr 9, 2026
Apr 9, 2026
79
class UnsortedTagsError(ValueError):
80
"""
81
Raised when a tag component is not in sorted order per PEP 425.
82
"""
83
84
Feb 6, 2021
Feb 6, 2021
85
class Tag:
Mar 23, 2020
Mar 23, 2020
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.
Apr 24, 2026
Apr 24, 2026
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.
Mar 23, 2020
Mar 23, 2020
101
"""
Aug 13, 2019
Aug 13, 2019
102
Jul 13, 2024
Jul 13, 2024
103
__slots__ = ["_abi", "_hash", "_interpreter", "_platform"]
Aug 13, 2019
Aug 13, 2019
104
Feb 9, 2021
Feb 9, 2021
105
def __init__(self, interpreter: str, abi: str, platform: str) -> None:
Mar 24, 2026
Mar 24, 2026
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
"""
Aug 19, 2019
Aug 19, 2019
114
self._interpreter = interpreter.lower()
115
self._abi = abi.lower()
116
self._platform = platform.lower()
Jun 25, 2020
Jun 25, 2020
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))
Aug 13, 2019
Aug 13, 2019
123
124
@property
Feb 9, 2021
Feb 9, 2021
125
def interpreter(self) -> str:
Mar 24, 2026
Mar 24, 2026
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
"""
Aug 13, 2019
Aug 13, 2019
131
return self._interpreter
132
133
@property
Feb 9, 2021
Feb 9, 2021
134
def abi(self) -> str:
Mar 24, 2026
Mar 24, 2026
135
"""
136
The supported ABI.
137
"""
Aug 13, 2019
Aug 13, 2019
138
return self._abi
139
140
@property
Feb 9, 2021
Feb 9, 2021
141
def platform(self) -> str:
Mar 24, 2026
Mar 24, 2026
142
"""
143
The OS/platform.
144
"""
Aug 13, 2019
Aug 13, 2019
145
return self._platform
146
Feb 9, 2021
Feb 9, 2021
147
def __eq__(self, other: object) -> bool:
Sep 19, 2019
Sep 19, 2019
148
if not isinstance(other, Tag):
149
return NotImplemented
150
Aug 13, 2019
Aug 13, 2019
151
return (
Mar 25, 2021
Mar 25, 2021
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)
Aug 13, 2019
Aug 13, 2019
156
)
157
Feb 9, 2021
Feb 9, 2021
158
def __hash__(self) -> int:
Jun 25, 2020
Jun 25, 2020
159
return self._hash
May 10, 2019
May 10, 2019
160
Feb 9, 2021
Feb 9, 2021
161
def __str__(self) -> str:
Feb 6, 2021
Feb 6, 2021
162
return f"{self._interpreter}-{self._abi}-{self._platform}"
May 10, 2019
May 10, 2019
163
Feb 9, 2021
Feb 9, 2021
164
def __repr__(self) -> str:
Oct 29, 2021
Oct 29, 2021
165
return f"<{self} @ {id(self)}>"
May 10, 2019
May 10, 2019
166
Apr 24, 2026
Apr 24, 2026
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}")
May 9, 2025
May 9, 2025
198
May 10, 2019
May 10, 2019
199
Apr 9, 2026
Apr 9, 2026
200
def parse_tag(tag: str, *, validate_order: bool = False) -> frozenset[Tag]:
Mar 23, 2020
Mar 23, 2020
201
"""
Mar 24, 2026
Mar 24, 2026
202
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of
203
:class:`Tag` instances.
Mar 23, 2020
Mar 23, 2020
204
205
Returning a set is required due to the possibility that the tag is a
Mar 24, 2026
Mar 24, 2026
206
`compressed tag set`_, e.g. ``"py2.py3-none-any"`` which supports both
207
Python 2 and Python 3.
208
Apr 9, 2026
Apr 9, 2026
209
If **validate_order** is true, compressed tag set components are checked
210
to be in sorted order as required by PEP 425.
211
Mar 24, 2026
Mar 24, 2026
212
:param str tag: The tag to parse, e.g. ``"py3-none-any"``.
Apr 9, 2026
Apr 9, 2026
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.
Mar 23, 2020
Mar 23, 2020
220
"""
May 10, 2019
May 10, 2019
221
tags = set()
222
interpreters, abis, platforms = tag.split("-")
Apr 9, 2026
Apr 9, 2026
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
)
May 10, 2019
May 10, 2019
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
Mar 10, 2024
Mar 10, 2024
237
def _get_config_var(name: str, warn: bool = False) -> int | str | None:
238
value: int | str | None = sysconfig.get_config_var(name)
Oct 1, 2019
Oct 1, 2019
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
Feb 9, 2021
Feb 9, 2021
246
def _normalize_string(string: str) -> str:
Feb 11, 2023
Feb 11, 2023
247
return string.replace(".", "_").replace("-", "_").replace(" ", "_")
May 10, 2019
May 10, 2019
248
249
Mar 10, 2024
Mar 10, 2024
250
def _is_threaded_cpython(abis: list[str]) -> bool:
Oct 23, 2023
Oct 23, 2023
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:
Dec 19, 2019
Dec 19, 2019
267
"""
268
Determine if the Python version supports abi3.
269
Apr 7, 2026
Apr 7, 2026
270
PEP 384 was first implemented in Python 3.2. The free-threaded
Oct 23, 2023
Oct 23, 2023
271
builds do not support abi3.
Dec 19, 2019
Dec 19, 2019
272
"""
Oct 23, 2023
Oct 23, 2023
273
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
Dec 19, 2019
Dec 19, 2019
274
275
Apr 7, 2026
Apr 7, 2026
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
Mar 10, 2024
Mar 10, 2024
289
def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]:
Nov 25, 2019
Nov 25, 2019
290
py_version = tuple(py_version) # To allow for version comparison.
Sep 13, 2019
Sep 13, 2019
291
abis = []
Feb 8, 2020
Feb 8, 2020
292
version = _version_nodot(py_version[:2])
Oct 23, 2023
Oct 23, 2023
293
threading = debug = pymalloc = ucs4 = ""
Oct 2, 2019
Oct 2, 2019
294
with_debug = _get_config_var("Py_DEBUG", warn)
Sep 13, 2019
Sep 13, 2019
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.
Sep 17, 2019
Sep 17, 2019
298
# https://github.com/pypa/pip/issues/3383#issuecomment-173267692
Sep 13, 2019
Sep 13, 2019
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"
Nov 21, 2023
Nov 21, 2023
302
if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn):
Oct 23, 2023
Oct 23, 2023
303
threading = "t"
Sep 13, 2019
Sep 13, 2019
304
if py_version < (3, 8):
Oct 2, 2019
Oct 2, 2019
305
with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
Sep 13, 2019
Sep 13, 2019
306
if with_pymalloc or with_pymalloc is None:
307
pymalloc = "m"
Sep 17, 2019
Sep 17, 2019
308
if py_version < (3, 3):
Oct 2, 2019
Oct 2, 2019
309
unicode_size = _get_config_var("Py_UNICODE_SIZE", warn)
Sep 17, 2019
Sep 17, 2019
310
if unicode_size == 4 or (
311
unicode_size is None and sys.maxunicode == 0x10FFFF
312
):
313
ucs4 = "u"
314
elif debug:
Sep 13, 2019
Sep 13, 2019
315
# Debug builds can also load "normal" extension modules.
316
# We can also assume no UCS-4 or pymalloc requirement.
Oct 23, 2023
Oct 23, 2023
317
abis.append(f"cp{version}{threading}")
318
abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}")
Sep 13, 2019
Sep 13, 2019
319
return abis
320
321
Nov 25, 2019
Nov 25, 2019
322
def cpython_tags(
Mar 10, 2024
Mar 10, 2024
323
python_version: PythonVersion | None = None,
324
abis: Iterable[str] | None = None,
325
platforms: Iterable[str] | None = None,
Feb 10, 2021
Feb 10, 2021
326
*,
327
warn: bool = False,
Feb 9, 2021
Feb 9, 2021
328
) -> Iterator[Tag]:
Nov 25, 2019
Nov 25, 2019
329
"""
Mar 24, 2026
Mar 24, 2026
330
Yields the tags for the CPython interpreter.
331
332
The specific tags generated are:
Nov 25, 2019
Nov 25, 2019
333
Mar 24, 2026
Mar 24, 2026
334
- ``cp<python_version>-<abi>-<platform>``
Apr 7, 2026
Apr 7, 2026
335
- ``cp<python_version>-<stable_abi>-<platform>``
Mar 24, 2026
Mar 24, 2026
336
- ``cp<python_version>-none-<platform>``
Apr 7, 2026
Apr 7, 2026
337
- ``cp<older version>-<stable_abi>-<platform>`` where "older version" is all older
Mar 24, 2026
Mar 24, 2026
338
minor versions down to Python 3.2 (when ``abi3`` was introduced)
Nov 25, 2019
Nov 25, 2019
339
Mar 24, 2026
Mar 24, 2026
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.
Nov 25, 2019
Nov 25, 2019
342
Apr 7, 2026
Apr 7, 2026
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
Mar 24, 2026
Mar 24, 2026
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``.
Nov 25, 2019
Nov 25, 2019
355
"""
356
if not python_version:
357
python_version = sys.version_info[:2]
358
Oct 29, 2021
Oct 29, 2021
359
interpreter = f"cp{_version_nodot(python_version[:2])}"
Nov 25, 2019
Nov 25, 2019
360
361
if abis is None:
Nov 12, 2025
Nov 12, 2025
362
abis = _cpython_abis(python_version, warn) if len(python_version) > 1 else []
Nov 25, 2019
Nov 25, 2019
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)
Nov 12, 2025
Nov 12, 2025
368
except ValueError: # noqa: PERF203
Nov 25, 2019
Nov 25, 2019
369
pass
370
Aug 27, 2021
Aug 27, 2021
371
platforms = list(platforms or platform_tags())
Sep 13, 2019
Sep 13, 2019
372
for abi in abis:
373
for platform_ in platforms:
374
yield Tag(interpreter, abi, platform_)
Oct 23, 2023
Oct 23, 2023
375
376
threading = _is_threaded_cpython(abis)
377
use_abi3 = _abi3_applies(python_version, threading)
Apr 7, 2026
Apr 7, 2026
378
use_abi3t = _abi3t_applies(python_version, threading)
379
Oct 23, 2023
Oct 23, 2023
380
if use_abi3:
Feb 6, 2021
Feb 6, 2021
381
yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
Apr 7, 2026
Apr 7, 2026
382
if use_abi3t:
383
yield from (Tag(interpreter, "abi3t", platform_) for platform_ in platforms)
384
Feb 6, 2021
Feb 6, 2021
385
yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
Dec 19, 2019
Dec 19, 2019
386
Apr 7, 2026
Apr 7, 2026
387
if use_abi3 or use_abi3t:
Nov 25, 2019
Nov 25, 2019
388
for minor_version in range(python_version[1] - 1, 1, -1):
389
for platform_ in platforms:
Aug 8, 2024
Aug 8, 2024
390
version = _version_nodot((python_version[0], minor_version))
391
interpreter = f"cp{version}"
Apr 7, 2026
Apr 7, 2026
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_)
May 10, 2019
May 10, 2019
399
400
Mar 10, 2024
Mar 10, 2024
401
def _generic_abi() -> list[str]:
Dec 23, 2022
Dec 23, 2022
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)]
May 10, 2019
May 10, 2019
440
441
Nov 25, 2019
Nov 25, 2019
442
def generic_tags(
Mar 10, 2024
Mar 10, 2024
443
interpreter: str | None = None,
444
abis: Iterable[str] | None = None,
445
platforms: Iterable[str] | None = None,
Feb 10, 2021
Feb 10, 2021
446
*,
447
warn: bool = False,
Feb 9, 2021
Feb 9, 2021
448
) -> Iterator[Tag]:
Nov 25, 2019
Nov 25, 2019
449
"""
Mar 24, 2026
Mar 24, 2026
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>``
May 10, 2019
May 10, 2019
459
Mar 24, 2026
Mar 24, 2026
460
The ``"none"`` ABI will be added if it was not explicitly provided.
May 10, 2019
May 10, 2019
461
Mar 24, 2026
Mar 24, 2026
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``.
Nov 25, 2019
Nov 25, 2019
469
"""
470
if not interpreter:
471
interp_name = interpreter_name()
472
interp_version = interpreter_version(warn=warn)
Nov 12, 2025
Nov 12, 2025
473
interpreter = f"{interp_name}{interp_version}"
Nov 12, 2025
Nov 12, 2025
474
abis = _generic_abi() if abis is None else list(abis)
Aug 27, 2021
Aug 27, 2021
475
platforms = list(platforms or platform_tags())
Nov 25, 2019
Nov 25, 2019
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_)
May 10, 2019
May 10, 2019
481
482
Feb 9, 2021
Feb 9, 2021
483
def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
May 10, 2019
May 10, 2019
484
"""
Nov 25, 2019
Nov 25, 2019
485
Yields Python versions in descending order.
May 10, 2019
May 10, 2019
486
487
After the latest version, the major-only version will be yielded, and then
Nov 25, 2019
Nov 25, 2019
488
all previous versions of that major version.
May 10, 2019
May 10, 2019
489
"""
Nov 25, 2019
Nov 25, 2019
490
if len(py_version) > 1:
Oct 29, 2021
Oct 29, 2021
491
yield f"py{_version_nodot(py_version[:2])}"
492
yield f"py{py_version[0]}"
Nov 25, 2019
Nov 25, 2019
493
if len(py_version) > 1:
494
for minor in range(py_version[1] - 1, -1, -1):
Oct 29, 2021
Oct 29, 2021
495
yield f"py{_version_nodot((py_version[0], minor))}"
May 10, 2019
May 10, 2019
496
497
Nov 25, 2019
Nov 25, 2019
498
def compatible_tags(
Mar 10, 2024
Mar 10, 2024
499
python_version: PythonVersion | None = None,
500
interpreter: str | None = None,
501
platforms: Iterable[str] | None = None,
Feb 9, 2021
Feb 9, 2021
502
) -> Iterator[Tag]:
May 10, 2019
May 10, 2019
503
"""
Mar 24, 2026
Mar 24, 2026
504
Yields the tags for an interpreter compatible with the Python version
505
specified by ``python_version``.
May 10, 2019
May 10, 2019
506
Mar 24, 2026
Mar 24, 2026
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.
May 10, 2019
May 10, 2019
520
"""
Nov 25, 2019
Nov 25, 2019
521
if not python_version:
522
python_version = sys.version_info[:2]
Aug 27, 2021
Aug 27, 2021
523
platforms = list(platforms or platform_tags())
Nov 25, 2019
Nov 25, 2019
524
for version in _py_interpreter_range(python_version):
May 10, 2019
May 10, 2019
525
for platform_ in platforms:
526
yield Tag(version, "none", platform_)
Nov 25, 2019
Nov 25, 2019
527
if interpreter:
528
yield Tag(interpreter, "none", "any")
529
for version in _py_interpreter_range(python_version):
May 10, 2019
May 10, 2019
530
yield Tag(version, "none", "any")
531
532
Feb 9, 2021
Feb 9, 2021
533
def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
Aug 25, 2019
Aug 25, 2019
534
if not is_32bit:
May 10, 2019
May 10, 2019
535
return arch
536
Aug 25, 2019
Aug 25, 2019
537
if arch.startswith("ppc"):
538
return "ppc"
539
540
return "i386"
541
May 10, 2019
May 10, 2019
542
Oct 2, 2024
Oct 2, 2024
543
def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:
May 10, 2019
May 10, 2019
544
formats = [cpu_arch]
545
if cpu_arch == "x86_64":
Aug 25, 2019
Aug 25, 2019
546
if version < (10, 4):
May 10, 2019
May 10, 2019
547
return []
Aug 25, 2019
Aug 25, 2019
548
formats.extend(["intel", "fat64", "fat32"])
549
May 10, 2019
May 10, 2019
550
elif cpu_arch == "i386":
Aug 25, 2019
Aug 25, 2019
551
if version < (10, 4):
May 10, 2019
May 10, 2019
552
return []
Aug 25, 2019
Aug 25, 2019
553
formats.extend(["intel", "fat32", "fat"])
554
May 10, 2019
May 10, 2019
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 []
Aug 25, 2019
Aug 25, 2019
559
formats.append("fat64")
560
May 10, 2019
May 10, 2019
561
elif cpu_arch == "ppc":
Aug 25, 2019
Aug 25, 2019
562
if version > (10, 6):
May 10, 2019
May 10, 2019
563
return []
Aug 25, 2019
Aug 25, 2019
564
formats.extend(["fat32", "fat"])
May 10, 2019
May 10, 2019
565
Nov 24, 2020
Nov 24, 2020
566
if cpu_arch in {"arm64", "x86_64"}:
567
formats.append("universal2")
568
Nov 30, 2020
Nov 30, 2020
569
if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}:
Nov 24, 2020
Nov 24, 2020
570
formats.append("universal")
571
May 10, 2019
May 10, 2019
572
return formats
573
574
Feb 9, 2021
Feb 9, 2021
575
def mac_platforms(
Oct 2, 2024
Oct 2, 2024
576
version: AppleVersion | None = None, arch: str | None = None
Feb 9, 2021
Feb 9, 2021
577
) -> Iterator[str]:
Nov 25, 2019
Nov 25, 2019
578
"""
Mar 24, 2026
Mar 24, 2026
579
Yields the :attr:`~Tag.platform` tags for macOS.
Nov 25, 2019
Nov 25, 2019
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.
Mar 24, 2026
Mar 24, 2026
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
Nov 25, 2019
Nov 25, 2019
598
"""
Feb 9, 2021
Feb 9, 2021
599
version_str, _, cpu_arch = platform.mac_ver()
May 10, 2019
May 10, 2019
600
if version is None:
Oct 2, 2024
Oct 2, 2024
601
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
Jun 17, 2022
Jun 17, 2022
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,
Aug 11, 2023
Aug 11, 2023
615
text=True,
Jun 17, 2022
Jun 17, 2022
616
).stdout
Oct 2, 2024
Oct 2, 2024
617
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
Nov 12, 2025
Nov 12, 2025
618
May 10, 2019
May 10, 2019
619
if arch is None:
Sep 19, 2019
Sep 19, 2019
620
arch = _mac_arch(cpu_arch)
Nov 24, 2020
Nov 24, 2020
621
Oct 21, 2025
Oct 21, 2025
622
if (10, 0) <= version < (11, 0):
Nov 24, 2020
Nov 24, 2020
623
# Prior to Mac OS 11, each yearly release of Mac OS bumped the
624
# "minor" version number. The major version was always 10.
Aug 8, 2024
Aug 8, 2024
625
major_version = 10
Nov 24, 2020
Nov 24, 2020
626
for minor_version in range(version[1], -1, -1):
Aug 8, 2024
Aug 8, 2024
627
compat_version = major_version, minor_version
Nov 24, 2020
Nov 24, 2020
628
binary_formats = _mac_binary_formats(compat_version, arch)
629
for binary_format in binary_formats:
Aug 8, 2024
Aug 8, 2024
630
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
Nov 24, 2020
Nov 24, 2020
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.
Aug 8, 2024
Aug 8, 2024
635
minor_version = 0
Nov 24, 2020
Nov 24, 2020
636
for major_version in range(version[0], 10, -1):
Aug 8, 2024
Aug 8, 2024
637
compat_version = major_version, minor_version
Nov 24, 2020
Nov 24, 2020
638
binary_formats = _mac_binary_formats(compat_version, arch)
639
for binary_format in binary_formats:
Aug 8, 2024
Aug 8, 2024
640
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
Nov 24, 2020
Nov 24, 2020
641
Jan 4, 2021
Jan 4, 2021
642
if version >= (11, 0):
Nov 24, 2020
Nov 24, 2020
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.
Jan 4, 2021
Jan 4, 2021
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.
Aug 8, 2024
Aug 8, 2024
650
major_version = 10
Jan 4, 2021
Jan 4, 2021
651
if arch == "x86_64":
652
for minor_version in range(16, 3, -1):
Aug 8, 2024
Aug 8, 2024
653
compat_version = major_version, minor_version
Jan 4, 2021
Jan 4, 2021
654
binary_formats = _mac_binary_formats(compat_version, arch)
655
for binary_format in binary_formats:
Aug 8, 2024
Aug 8, 2024
656
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
Jan 4, 2021
Jan 4, 2021
657
else:
658
for minor_version in range(16, 3, -1):
Aug 8, 2024
Aug 8, 2024
659
compat_version = major_version, minor_version
Jan 4, 2021
Jan 4, 2021
660
binary_format = "universal2"
Aug 8, 2024
Aug 8, 2024
661
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
May 10, 2019
May 10, 2019
662
663
Oct 2, 2024
Oct 2, 2024
664
def ios_platforms(
665
version: AppleVersion | None = None, multiarch: str | None = None
666
) -> Iterator[str]:
667
"""
668
Mar 24, 2026
Mar 24, 2026
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.
Oct 2, 2024
Oct 2, 2024
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.
Nov 4, 2024
Nov 4, 2024
686
_, release, _, _ = platform.ios_ver() # type: ignore[attr-defined, unused-ignore]
Oct 2, 2024
Oct 2, 2024
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
Apr 15, 2025
Apr 15, 2025
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
Feb 9, 2021
Feb 9, 2021
763
def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
Apr 23, 2021
Apr 23, 2021
764
linux = _normalize_string(sysconfig.get_platform())
Jul 14, 2023
Jul 14, 2023
765
if not linux.startswith("linux_"):
766
# we should never be here, just yield the sysconfig one and return
767
yield linux
768
return
Feb 6, 2020
Feb 6, 2020
769
if is_32bit:
770
if linux == "linux_x86_64":
771
linux = "linux_i686"
772
elif linux == "linux_aarch64":
Jul 14, 2023
Jul 14, 2023
773
linux = "linux_armv8l"
Jan 2, 2020
Jan 2, 2020
774
_, arch = linux.split("_", 1)
Jul 14, 2023
Jul 14, 2023
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}"
May 10, 2019
May 10, 2019
780
781
Apr 8, 2026
Apr 8, 2026
782
def _emscripten_platforms() -> Iterator[str]:
Apr 15, 2026
Apr 15, 2026
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"
Apr 8, 2026
Apr 8, 2026
788
yield from _generic_platforms()
789
790
Feb 9, 2021
Feb 9, 2021
791
def _generic_platforms() -> Iterator[str]:
Apr 23, 2021
Apr 23, 2021
792
yield _normalize_string(sysconfig.get_platform())
Nov 25, 2019
Nov 25, 2019
793
May 10, 2019
May 10, 2019
794
Aug 27, 2021
Aug 27, 2021
795
def platform_tags() -> Iterator[str]:
Nov 25, 2019
Nov 25, 2019
796
"""
Mar 24, 2026
Mar 24, 2026
797
Yields the :attr:`~Tag.platform` tags for the running interpreter.
Nov 25, 2019
Nov 25, 2019
798
"""
799
if platform.system() == "Darwin":
800
return mac_platforms()
Oct 2, 2024
Oct 2, 2024
801
elif platform.system() == "iOS":
802
return ios_platforms()
Apr 15, 2025
Apr 15, 2025
803
elif platform.system() == "Android":
804
return android_platforms()
Nov 25, 2019
Nov 25, 2019
805
elif platform.system() == "Linux":
806
return _linux_platforms()
Apr 8, 2026
Apr 8, 2026
807
elif platform.system() == "Emscripten":
808
return _emscripten_platforms()
Nov 25, 2019
Nov 25, 2019
809
else:
810
return _generic_platforms()
May 10, 2019
May 10, 2019
811
Nov 25, 2019
Nov 25, 2019
812
Feb 9, 2021
Feb 9, 2021
813
def interpreter_name() -> str:
Nov 25, 2019
Nov 25, 2019
814
"""
815
Returns the name of the running interpreter.
Dec 23, 2022
Dec 23, 2022
816
817
Some implementations have a reserved, two-letter abbreviation which will
818
be returned when appropriate.
Mar 24, 2026
Mar 24, 2026
819
820
This typically acts as the prefix to the :attr:`~Tag.interpreter` tag.
Nov 25, 2019
Nov 25, 2019
821
"""
Feb 6, 2021
Feb 6, 2021
822
name = sys.implementation.name
May 10, 2019
May 10, 2019
823
return INTERPRETER_SHORT_NAMES.get(name) or name
824
825
Feb 10, 2021
Feb 10, 2021
826
def interpreter_version(*, warn: bool = False) -> str:
Nov 25, 2019
Nov 25, 2019
827
"""
Mar 24, 2026
Mar 24, 2026
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``.
Nov 25, 2019
Nov 25, 2019
833
"""
834
version = _get_config_var("py_version_nodot", warn=warn)
Nov 12, 2025
Nov 12, 2025
835
return str(version) if version else _version_nodot(sys.version_info[:2])
May 10, 2019
May 10, 2019
836
837
Feb 9, 2021
Feb 9, 2021
838
def _version_nodot(version: PythonVersion) -> str:
Dec 11, 2020
Dec 11, 2020
839
return "".join(map(str, version))
Feb 8, 2020
Feb 8, 2020
840
841
Feb 10, 2021
Feb 10, 2021
842
def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
May 10, 2019
May 10, 2019
843
"""
Mar 24, 2026
Mar 24, 2026
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``.
May 10, 2019
May 10, 2019
864
Mar 24, 2026
Mar 24, 2026
865
.. versionchanged:: 21.3
866
Added the `pp3-none-any` tag (:issue:`311`).
Apr 7, 2026
Apr 7, 2026
867
.. versionchanged:: 27.0
868
Added the `abi3t` tag (:issue:`1099`).
May 10, 2019
May 10, 2019
869
"""
870
Nov 25, 2019
Nov 25, 2019
871
interp_name = interpreter_name()
872
if interp_name == "cp":
Feb 6, 2021
Feb 6, 2021
873
yield from cpython_tags(warn=warn)
May 10, 2019
May 10, 2019
874
else:
Feb 6, 2021
Feb 6, 2021
875
yield from generic_tags()
Nov 25, 2019
Nov 25, 2019
876
Oct 29, 2021
Oct 29, 2021
877
if interp_name == "pp":
Aug 19, 2022
Aug 19, 2022
878
interp = "pp3"
879
elif interp_name == "cp":
880
interp = "cp" + interpreter_version(warn=warn)
Oct 29, 2021
Oct 29, 2021
881
else:
Aug 19, 2022
Aug 19, 2022
882
interp = None
883
yield from compatible_tags(interpreter=interp)
Mar 26, 2026
Mar 26, 2026
884
885
886
def create_compatible_tags_selector(
887
tags: Iterable[Tag],
Mar 26, 2026
Mar 26, 2026
888
) -> Callable[[Iterable[tuple[_T, AbstractSet[Tag]]]], Iterator[_T]]:
Mar 26, 2026
Mar 26, 2026
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
Mar 26, 2026
Mar 26, 2026
894
The returned callable accepts an iterable of tuples (thing, set[Tag]),
Mar 26, 2026
Mar 26, 2026
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(
Mar 26, 2026
Mar 26, 2026
917
tagged_things: Iterable[tuple[_T, AbstractSet[Tag]]],
Mar 26, 2026
Mar 26, 2026
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