Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/packaging/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,23 @@
import sysconfig
from importlib.machinery import EXTENSION_SUFFIXES
from typing import (
TYPE_CHECKING,
Any,
Iterable,
Iterator,
Sequence,
Tuple,
TypeVar,
cast,
)

from . import _manylinux, _musllinux

if TYPE_CHECKING:
from collections.abc import Callable, Collection, Iterable
from typing import AbstractSet


__all__ = [
"INTERPRETER_SHORT_NAMES",
"AppleVersion",
Expand Down Expand Up @@ -50,6 +57,7 @@ def __dir__() -> list[str]:

PythonVersion = Sequence[int]
AppleVersion = Tuple[int, int]
_T = TypeVar("_T")

INTERPRETER_SHORT_NAMES: dict[str, str] = {
"python": "py", # Generic.
Expand Down Expand Up @@ -672,3 +680,43 @@ def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
else:
interp = None
yield from compatible_tags(interpreter=interp)


def create_compatible_tags_selector(
tags: Iterable[Tag],
) -> Callable[[Collection[tuple[_T, AbstractSet[Tag]]]], Iterator[_T]]:
"""Create a callable to select things compatible with supported tags.

This function accepts an ordered sequence of tags, with the preferred
tags first.

The returned callable accepts a collection of tuples (thing, set[Tag]),
and returns an iterator of things, with the things with the best
matching tags first.

Example to select compatible wheel filenames:

>>> filenames = ["foo-1.0-py3-none-any.whl", "foo-1.0-py2-none-any.whl"]
>>> selector = create_compatible_tags_selector(tags.sys_tags())
>>> compatible_filenames = list(selector([
... (filename, parse_wheel_filename(filename)[-1]) for filename in filenames
... ]))
["foo-1.0-py3-none-any.whl"]
"""
tag_ranks: dict[Tag, int] = {}
for rank, tag in enumerate(tags):
tag_ranks.setdefault(tag, rank) # ignore duplicate tags, keep first
supported_tags = tag_ranks.keys()

def selector(
tagged_things: Collection[tuple[_T, AbstractSet[Tag]]],
) -> Iterator[_T]:
ranked_things: list[tuple[_T, int]] = []
for thing, thing_tags in tagged_things:
supported_thing_tags = thing_tags & supported_tags
if supported_thing_tags:
thing_rank = min(tag_ranks[t] for t in supported_thing_tags)
ranked_things.append((thing, thing_rank))
return iter(thing for thing, _ in sorted(ranked_things, key=lambda rt: rt[1]))

return selector
25 changes: 25 additions & 0 deletions tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -1694,3 +1694,28 @@ def test_pickle() -> None:
# Make sure equality works between a pickle/unpickle round trip.
tag = tags.Tag("py3", "none", "any")
assert pickle.loads(pickle.dumps(tag)) == tag


@pytest.mark.parametrize(
("supported", "things", "expected"),
[
(["t1", "t2"], ["t1", "t2"], ["t1", "t2"]),
(["t1", "t2"], ["t3", "t4"], []),
(["t1", "t2"], ["t2", "t1"], ["t1", "t2"]),
(["t1", "t2", "t1"], ["t2", "t1"], ["t1", "t2"]),
(["t1", "t3"], ["t2", "t1"], ["t1"]),
(["t1", "t3"], ["t2.t3", "t1"], ["t1", "t2.t3"]),
(["t1"], ["t2", "t1"], ["t1"]),
],
)
def test_create_compatible_tags_selector(
supported: list[str], things: list[str], expected: list[str]
) -> None:
def t_to_tag(t: str) -> tags.Tag:
return tags.Tag("py3", "none", t)

def t_to_tags(t: str) -> frozenset[tags.Tag]:
return tags.parse_tag(f"py3-none-{t}")

selector = tags.create_compatible_tags_selector([t_to_tag(t) for t in supported])
assert list(selector([(t, t_to_tags(t)) for t in things])) == expected
Loading