Skip to content

Commit 8a3a7ec

Browse files
committed
Add compatible tags selector utility
1 parent 55dfb87 commit 8a3a7ec

File tree

2 files changed

+71
-0
lines changed

2 files changed

+71
-0
lines changed

src/packaging/tags.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,23 @@
1313
import sysconfig
1414
from importlib.machinery import EXTENSION_SUFFIXES
1515
from typing import (
16+
TYPE_CHECKING,
1617
Any,
1718
Iterable,
1819
Iterator,
1920
Sequence,
2021
Tuple,
22+
TypeVar,
2123
cast,
2224
)
2325

2426
from . import _manylinux, _musllinux
2527

28+
if TYPE_CHECKING:
29+
from collections.abc import Callable, Collection, Iterable
30+
from typing import AbstractSet
31+
32+
2633
__all__ = [
2734
"INTERPRETER_SHORT_NAMES",
2835
"AppleVersion",
@@ -50,6 +57,7 @@ def __dir__() -> list[str]:
5057

5158
PythonVersion = Sequence[int]
5259
AppleVersion = Tuple[int, int]
60+
_T = TypeVar("_T")
5361

5462
INTERPRETER_SHORT_NAMES: dict[str, str] = {
5563
"python": "py", # Generic.
@@ -672,3 +680,43 @@ def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
672680
else:
673681
interp = None
674682
yield from compatible_tags(interpreter=interp)
683+
684+
685+
def create_compatible_tags_selector(
686+
tags: Iterable[Tag],
687+
) -> Callable[[Collection[tuple[_T, AbstractSet[Tag]]]], Sequence[_T]]:
688+
"""Create a callable to select things compatible with supported tags.
689+
690+
This function accepts an ordered sequence of tags, with the preferred
691+
tags first.
692+
693+
The returned callable accepts a collection of tuples (thing, set[Tag]),
694+
and returns an ordered sequence of things, with the things with the best
695+
matching tags first. The returned sequence is empty if nothing matches.
696+
697+
Example to select compatible wheel filenames:
698+
699+
>>> filenames = ["foo-1.0-py3-none-any.whl", "foo-1.0-py2-none-any.whl"]
700+
>>> selector = create_compatible_tags_selector(tags.sys_tags())
701+
>>> compatible_filenames = selector([
702+
... (filename, parse_wheel_filename(filename)[-1]) for filename in filenames
703+
... ])
704+
["foo-1.0-py3-none-any.whl"]
705+
"""
706+
tag_ranks: dict[Tag, int] = {}
707+
for rank, tag in enumerate(tags):
708+
tag_ranks.setdefault(tag, rank) # ignore duplicate tags, keep first
709+
supported_tags = tag_ranks.keys()
710+
711+
def selector(
712+
tagged_things: Collection[tuple[_T, AbstractSet[Tag]]],
713+
) -> Sequence[_T]:
714+
ranked_things: list[tuple[_T, int]] = []
715+
for thing, thing_tags in tagged_things:
716+
supported_thing_tags = thing_tags & supported_tags
717+
if supported_thing_tags:
718+
thing_rank = min(tag_ranks[t] for t in supported_thing_tags)
719+
ranked_things.append((thing, thing_rank))
720+
return [thing for thing, _ in sorted(ranked_things, key=lambda rt: rt[1])]
721+
722+
return selector

tests/test_tags.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,3 +1694,26 @@ def test_pickle() -> None:
16941694
# Make sure equality works between a pickle/unpickle round trip.
16951695
tag = tags.Tag("py3", "none", "any")
16961696
assert pickle.loads(pickle.dumps(tag)) == tag
1697+
1698+
1699+
@pytest.mark.parametrize(
1700+
("supported", "things", "expected"),
1701+
[
1702+
(["t1", "t2"], ["t1", "t2"], ["t1", "t2"]),
1703+
(["t1", "t2"], ["t2", "t1"], ["t1", "t2"]),
1704+
(["t1", "t3"], ["t2", "t1"], ["t1"]),
1705+
(["t1", "t3"], ["t2.t3", "t1"], ["t1", "t2.t3"]),
1706+
(["t1"], ["t2", "t1"], ["t1"]),
1707+
],
1708+
)
1709+
def test_create_compatible_tags_selector(
1710+
supported: list[str], things: list[str], expected: list[str]
1711+
) -> None:
1712+
def t_to_tag(t: str) -> tags.Tag:
1713+
return tags.Tag("py3", "none", t)
1714+
1715+
def t_to_tags(t: str) -> frozenset[tags.Tag]:
1716+
return tags.parse_tag(f"py3-none-{t}")
1717+
1718+
selector = tags.create_compatible_tags_selector([t_to_tag(t) for t in supported])
1719+
assert selector([(t, t_to_tags(t)) for t in things]) == expected

0 commit comments

Comments
 (0)