Repository: pex-tool/pex

Issue:
Resolve selection for universal locks needs its own logic.
Unlike platform specific locks, universal locks only ever will have 1 locked resolve contained within - the universal resolve. That resolve should always be picked without ranking dists; only the `requires_python`, if present, should be used to fail fast in the case that the targets don't comply.


Discussion:
The groundwork for this was layed in #1625 with the introductio of `LockedResolve.resolve`.
https://github.com/pantsbuild/pex/blob/2e2d1e50e604fdee48b0d51aea482ca255521ff0/pex/resolve/locked_resolve.py#L435-L445

All that's needed now is replacing `Lockfile.select` with it:
https://github.com/pantsbuild/pex/blob/2e2d1e50e604fdee48b0d51aea482ca255521ff0/pex/resolve/lockfile/lockfile.py#L139-L149

Resolving patch:
diff --git a/pex/resolve/locked_resolve.py b/pex/resolve/locked_resolve.py
--- a/pex/resolve/locked_resolve.py
+++ b/pex/resolve/locked_resolve.py
@@ -1,7 +1,7 @@
 # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
-from __future__ import absolute_import
+from __future__ import absolute_import, division
 
 import itertools
 import os
@@ -294,6 +294,7 @@ class Resolved(object):
     @classmethod
     def create(
         cls,
+        target,  # type: Target
         direct_requirements,  # type: Iterable[Requirement]
         downloadable_requirements,  # type: Iterable[_ResolvedArtifact]
     ):
@@ -307,7 +308,14 @@ def create(
                 requirement
             )
 
+        # N.B.: Lowest rank means highest rank value. I.E.: The 1st tag is the most specific and
+        # the 765th tag is the least specific.
+        largest_rank_value = target.supported_tags.lowest_rank.value
+        smallest_rank_value = TagRank.highest_natural().value
+        rank_span = largest_rank_value - smallest_rank_value
+
         downloadable_artifacts = []
+        target_specificities = []
         for downloadable_requirement in downloadable_requirements:
             pin = downloadable_requirement.locked_requirement.pin
             downloadable_artifacts.append(
@@ -319,11 +327,20 @@ def create(
                     ],
                 )
             )
+            target_specificities.append(
+                (
+                    rank_span
+                    - (downloadable_requirement.ranked_artifact.rank.value - smallest_rank_value)
+                )
+                / rank_span
+            )
 
         return cls(
+            target_specificity=sum(target_specificities) / len(target_specificities),
             downloadable_artifacts=tuple(downloadable_artifacts),
         )
 
+    target_specificity = attr.ib()  # type: float
     downloadable_artifacts = attr.ib()  # type: Tuple[DownloadableArtifact, ...]
 
 
@@ -630,6 +647,7 @@ def attributed_reason(reason):
             )
 
         return Resolved.create(
+            target=target,
             direct_requirements=requirements,
             downloadable_requirements=resolved_artifacts,
         )
diff --git a/pex/resolve/lockfile/lockfile.py b/pex/resolve/lockfile/lockfile.py
--- a/pex/resolve/lockfile/lockfile.py
+++ b/pex/resolve/lockfile/lockfile.py
@@ -3,7 +3,7 @@
 
 from __future__ import absolute_import, print_function
 
-from pex.resolve.locked_resolve import LockedResolve, LockStyle
+from pex.resolve.locked_resolve import LockedResolve, LockStyle, Resolved
 from pex.resolve.resolver_configuration import ResolverVersion
 from pex.sorted_tuple import SortedTuple
 from pex.targets import Target
@@ -19,67 +19,6 @@
     from pex.third_party import attr
 
 
-@attr.s(frozen=True)
-class _RankedLock(object):
-    @classmethod
-    def rank(
-        cls,
-        locked_resolve,  # type: LockedResolve
-        target,  # type: Target
-    ):
-        # type: (...) -> Optional[_RankedLock]
-        """Rank the given resolve for the supported tags of a distribution target.
-
-        Pex allows choosing an array of distribution targets as part of building multiplatform PEX
-        files. Whether via interpreter constraint ranges, multiple `--python` or `--platform`
-        specifications or some combination of these, parallel resolves will be executed for each
-        distinct distribution target selected. When generating a lock, Pex similarly will create a
-        locked resolve per selected distribution target in parallel. Later, at lock consumption
-        time, there will again be one or more distribution targets that may need to resolve from the
-        lock. For each of these distribution targets, either one or more of the generated locks will
-        be applicable or none will. If the distribution target matches one of those used to generate
-        the lock file, the corresponding locked resolve will clearly work. The distribution target
-        need not match though for the locked resolve to be usable. All that's needed is for at least
-        one artifact for each locked requirement in the resolve to be usable by the distribution
-        target. The classic example is a locked resolve that is populated with only universal
-        wheels. Even if such a locked resolve was generated by a PyPy 2 interpreter, it should be
-        usable by a CPython 3.10 interpreter or any other Python 2 or Python 3 interpreter.
-
-        To help select which locked resolve to use, ranking gives a score to each locked resolve
-        that is the average of the score of each locked requirement in the resolve. Each locked
-        requirement is, in turn, scored by its best matching artifact score. Artifacts are scored as
-        follows:
-
-        + If the artifact is a wheel, score it based on its best matching tag.
-        + If the artifact is an sdist, score it as usable, but a worse match than any wheel.
-        + Otherwise treat the artifact as unusable.
-
-        If a locked requirement has no matching artifact, the scoring is aborted since the locked
-        resolve has an unsatisfied requirement and `None` is returned.
-
-        :param locked_resolve: The resolve to rank.
-        :param target: The target looking to pick a resolve to use.
-        :return: A ranked lock if the resolve is applicable to the target else `None`.
-        """
-        requirement_ranks = []
-        for req in locked_resolve.locked_requirements:
-            ranked_artifact = req.select_artifact(target)
-            if not ranked_artifact:
-                return None
-            requirement_ranks.append(ranked_artifact.rank)
-
-        if not requirement_ranks:
-            return None
-
-        average_requirement_rank = float(sum(rank.value for rank in requirement_ranks)) / len(
-            locked_resolve.locked_requirements
-        )
-        return cls(average_requirement_rank=average_requirement_rank, locked_resolve=locked_resolve)
-
-    average_requirement_rank = attr.ib()  # type: float
-    locked_resolve = attr.ib()  # type: LockedResolve
-
-
 @attr.s(frozen=True)
 class Lockfile(object):
     @classmethod
@@ -150,29 +89,24 @@ def select(self, targets):
 
     def _select(self, target):
         # type: (Target) -> Optional[LockedResolve]
-        ranked_locks = []  # type: List[_RankedLock]
-
+        resolves = []  # type: List[Tuple[float, LockedResolve]]
         for locked_resolve in self.locked_resolves:
-            ranked_lock = _RankedLock.rank(locked_resolve, target)
-            if ranked_lock is not None:
-                ranked_locks.append(ranked_lock)
+            result = locked_resolve.resolve(target, self.requirements)
+            if isinstance(result, Resolved):
+                resolves.append((result.target_specificity, locked_resolve))
 
-        if not ranked_locks:
+        if not resolves:
             return None
 
-        ranked_lock = sorted(ranked_locks)[0]
-        count = len(target.supported_tags)
+        target_specificity, locked_resolve = sorted(resolves)[-1]
         TRACER.log(
-            "Selected lock generated by {platform} with an average requirement rank of "
-            "{average_requirement_rank:.2f} (out of {count}, so ~{percent:.1%} platform specific) "
-            "from locks generated by {platforms}".format(
-                platform=ranked_lock.locked_resolve.platform_tag,
-                average_requirement_rank=ranked_lock.average_requirement_rank,
-                count=count,
-                percent=(count - ranked_lock.average_requirement_rank) / count,
+            "Selected lock generated by {platform} with an average artifact platform specificity "
+            "of ~{percent:.1%} from locks generated by {platforms}".format(
+                platform=locked_resolve.platform_tag,
+                percent=target_specificity,
                 platforms=", ".join(
                     sorted(str(lock.platform_tag) for lock in self.locked_resolves)
                 ),
             )
         )
-        return ranked_lock.locked_resolve
+        return locked_resolve


Test patch:
diff --git a/tests/resolve/test_locked_resolve.py b/tests/resolve/test_locked_resolve.py
--- a/tests/resolve/test_locked_resolve.py
+++ b/tests/resolve/test_locked_resolve.py
@@ -6,9 +6,10 @@
 
 from pex import targets
 from pex.interpreter import PythonInterpreter
-from pex.pep_425 import TagRank
+from pex.pep_425 import CompatibilityTags, TagRank
 from pex.pep_440 import Version
 from pex.pep_503 import ProjectName
+from pex.pep_508 import MarkerEnvironment
 from pex.platforms import Platform
 from pex.resolve.locked_resolve import (
     Artifact,
@@ -22,7 +23,7 @@
 from pex.resolve.resolved_requirement import Fingerprint, Pin
 from pex.result import Error, try_
 from pex.sorted_tuple import SortedTuple
-from pex.targets import AbbreviatedPlatform, LocalInterpreter, Target
+from pex.targets import AbbreviatedPlatform, CompletePlatform, LocalInterpreter, Target
 from pex.third_party.packaging.specifiers import SpecifierSet
 from pex.third_party.packaging.tags import Tag
 from pex.third_party.pkg_resources import Requirement
@@ -692,3 +693,59 @@ def test_multiple_errors(
             """
         ).format(target_description=current_target.render_description()),
     )
+
+
+def test_resolved():
+    # type: () -> None
+
+    def assert_resolved(
+        expected_target_specificity,  # type: float
+        supported_tag_count,  # type: int
+        artifact_ranks,  # type: Iterable[int]
+    ):
+        # type: (...) -> None
+        direct_requirements = requirements("foo")
+
+        resolved_artifacts = tuple(
+            resolved_artifact(
+                project_name="foo",
+                version=str(rank),
+                artifact_basename="foo-{}.tar.gz".format(rank),
+                rank_value=rank,
+            )
+            for rank in artifact_ranks
+        )
+
+        downloadable_artifacts = tuple(
+            DownloadableArtifact.create(
+                pin=resolved_art.locked_requirement.pin,
+                artifact=resolved_art.artifact,
+                satisfied_direct_requirements=direct_requirements,
+            )
+            for resolved_art in resolved_artifacts
+        )
+
+        target = CompletePlatform.create(
+            MarkerEnvironment(),
+            CompatibilityTags.from_strings(
+                "py3-none-manylinux_2_{glibc_minor}_x86_64".format(glibc_minor=glibc_minor)
+                for glibc_minor in range(supported_tag_count)
+            ),
+        )
+
+        assert Resolved(
+            target_specificity=expected_target_specificity,
+            downloadable_artifacts=downloadable_artifacts,
+        ) == Resolved.create(
+            target=target,
+            direct_requirements=direct_requirements,
+            downloadable_requirements=resolved_artifacts,
+        )
+
+    # For tag ranks of 1, 2, 1 should rank 100% target specific (best match) and 2 should rank 0%
+    # (worst match / universal)
+    assert_resolved(expected_target_specificity=1.0, supported_tag_count=2, artifact_ranks=[1])
+    assert_resolved(expected_target_specificity=0.0, supported_tag_count=2, artifact_ranks=[2])
+
+    # For tag ranks of 1, 2, 3, 2 lands in the middle and should be 50% target specific.
+    assert_resolved(expected_target_specificity=0.5, supported_tag_count=3, artifact_ranks=[2])