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
2 changes: 2 additions & 0 deletions docs/tags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ items that applications should need to reference, in order to parse and check ta

.. autofunction:: sys_tags

.. autofunction:: create_compatible_tags_selector



Low Level Interface
Expand Down
56 changes: 56 additions & 0 deletions src/packaging/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import annotations

import logging
import operator
import platform
import re
import struct
Expand All @@ -13,16 +14,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 All @@ -31,6 +39,7 @@
"android_platforms",
"compatible_tags",
"cpython_tags",
"create_compatible_tags_selector",
"generic_tags",
"interpreter_name",
"interpreter_version",
Expand All @@ -50,6 +59,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 @@ -769,3 +779,49 @@ 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:

>>> from packaging import tags
>>> from packaging.utils import parse_wheel_filename
>>> selector = tags.create_compatible_tags_selector(tags.sys_tags())
>>> filenames = ["foo-1.0-py3-none-any.whl", "foo-1.0-py2-none-any.whl"]
>>> list(selector([
... (filename, parse_wheel_filename(filename)[-1]) for filename in filenames
... ]))
['foo-1.0-py3-none-any.whl']

.. versionadded:: 26.1
"""
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=operator.itemgetter(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