Repository: pypa/pip

Issue:
Sort wheel files such that the build number is used only as a tie-breaker
**pip version**

21.0.1

**Python version**

N/A

**OS**

N/A

**Additional information**

_No response_

**Description**

Pip currently [sorts a wheel](https://github.com/pypa/pip/blob/a48ad5385b234097d51283b08c3d933fd81ef534/src/pip/_internal/index/package_finder.py#L537-L540)'s build number higher than the other tags in a wheel.



**Expected behavior**

The build number should be used to break ties between wheel files when all other details are equal (i.e. name, version, and other tags). See the [update to PEP 427](https://github.com/python/peps/commit/9e81dc8092505810dc144ccf6c4adc8f1a3becba) for more details.

**How to Reproduce**

_No response_

**Output**

_No response_

**Code of Conduct**

- [X] I agree to follow the PSF Code of Conduct



Discussion:
pfmoore: There's also a few lint issues to be fixed.
pradyunsg: It might be worthwhile to think about / investigate if there's folks doing things in a weird way here (and decide if we need a deprecation cycle here).

I don't have any opinions on this, but just flagging this because it feels like a case where we'd create nuanced failures for someone.
uranusjr: I seem to recall there was a research on PyPI wheels and basically nobody is using build tags right now. If that’s correct, anybody doing weird things are doing them in private indexes.

If past experience of how those people react to similar behavioural changes, I’m pessimistic how effective a deprecation cycle would be.
hexagonrecursion: > It might be worthwhile to think about / investigate if there's folks doing things in a weird way here (and decide if we need a deprecation cycle here).

Should I reclassify this a breaking instead of bugfix?
hexagonrecursion: > the current test would pass without flipping the sort logic, if I understand correctly.

I believe this is incorrect. I ran the test without the code change and got:
```
___ TestCandidateEvaluator.test_build_tag_is_less_important_than_other_tags ____

self = <tests.unit.test_finder.TestCandidateEvaluator object at 0x729ce6dbd130>

    def test_build_tag_is_less_important_than_other_tags(self):
        links = [
            InstallationCandidate(
                "simple",
                "1.0",
                Link('simple-1.0-1-py3-abi3-linux_x86_64.whl'),
            ),
            InstallationCandidate(
                "simple",
                '1.0',
                Link('simple-1.0-2-py3-abi3-linux_i386.whl'),
            ),
            InstallationCandidate(
                "simple",
                '1.0',
                Link('simple-1.0.tar.gz'),
            ),
        ]
        valid_tags = [
            Tag('py3', 'abi3', 'linux_x86_64'),
            Tag('py3', 'abi3', 'linux_i386'),
        ]
        evaluator = CandidateEvaluator(
            'my-project', supported_tags=valid_tags, specifier = SpecifierSet(),
        )
        sort_key = evaluator._sort_key
        results = sorted(links, key=sort_key, reverse=True)
        results2 = sorted(reversed(links), key=sort_key, reverse=True)
    
>       assert links == results == results2, results2
E       AssertionError: [<InstallationCandidate('simple', <Version('1.0')>, <Link simple-1.0-2-py3-abi3-linux_i386.whl>)>, <InstallationCandid...mple-1.0-1-py3-abi3-linux_x86_64.whl>)>, <InstallationCandidate('simple', <Version('1.0')>, <Link simple-1.0.tar.gz>)>]
E       assert [<Installatio...1.0.tar.gz>)>] == [<Installatio...1.0.tar.gz>)>]
E         At index 0 diff: <InstallationCandidate('simple', <Version('1.0')>, <Link simple-1.0-1-py3-abi3-linux_x86_64.whl>)> != <InstallationCandidate('simple', <Version('1.0')>, <Link simple-1.0-2-py3-abi3-linux_i386.whl>)>
E         Use -v to get the full diff

tests/unit/test_finder.py:310: AssertionError
```
hexagonrecursion: > At least one more test (or fixture) is needed to ensure `1.0-1-py3-abi3-linux_x86_64.whl` would be prioritised over `1.0-2-py3-any-none.whl`.

done
pradyunsg: I'm pretty sure PyPI disallows wheels containing build tags, so this shouldn't affect PyPI-only users.
hexagonrecursion: > If past experience of how those people react to similar behavioural changes, I’m pessimistic how effective a deprecation cycle would be.

Bias alert: you hear complaints from people who ignored the deprecation until you broke their code but you never hear anything from people who headed your warning and fixed things on their end in time.
BrownTruck: Hello!

I am an automated bot and I have noticed that this pull request is not currently able to be merged. If you are able to either merge the ``master`` branch into this pull request or rebase this pull request against ``master`` then it will be eligible for code review and hopefully merging!
pradyunsg: @hexagonrecursion Could you update this PR?

Patch:
diff --git a/news/9565.bugfix.rst b/news/9565.bugfix.rst
new file mode 100644
index 000000000..b8f95fd35
--- /dev/null
+++ b/news/9565.bugfix.rst
@@ -0,0 +1,1 @@
+Make wheel compatibility tag preferences more important than the build tag
diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py
index b826690fa..4cc9ffd4d 100644
--- a/src/pip/_internal/index/package_finder.py
+++ b/src/pip/_internal/index/package_finder.py
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
 
 BuildTag = Union[Tuple[()], Tuple[int, str]]
 CandidateSortingKey = (
-    Tuple[int, int, int, _BaseVersion, BuildTag, Optional[int]]
+    Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
 )
 
 
@@ -530,7 +530,7 @@ class CandidateEvaluator:
         yank_value = -1 * int(link.is_yanked)  # -1 for yanked.
         return (
             has_allowed_hash, yank_value, binary_preference, candidate.version,
-            build_tag, pri,
+            pri, build_tag,
         )
 
     def sort_best_candidate(


Test patch:
diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py
index 162a9b356..9638199fb 100644
--- a/tests/unit/test_finder.py
+++ b/tests/unit/test_finder.py
@@ -209,6 +209,8 @@ class TestWheel:
         with pytest.raises(BestVersionAlreadyInstalled):
             finder.find_requirement(req, True)
 
+
+class TestCandidateEvaluator:
     def test_link_sorting(self):
         """
         Test link sorting
@@ -249,7 +251,8 @@ class TestWheel:
         results = sorted(links, key=sort_key, reverse=True)
         results2 = sorted(reversed(links), key=sort_key, reverse=True)
 
-        assert links == results == results2, results2
+        assert links == results, results
+        assert links == results2, results2
 
     def test_link_sorting_wheels_with_build_tags(self):
         """Verify build tags affect sorting."""
@@ -274,7 +277,47 @@ class TestWheel:
         sort_key = candidate_evaluator._sort_key
         results = sorted(links, key=sort_key, reverse=True)
         results2 = sorted(reversed(links), key=sort_key, reverse=True)
-        assert links == results == results2, results2
+
+        assert links == results, results
+        assert links == results2, results2
+
+    def test_build_tag_is_less_important_than_other_tags(self):
+        links = [
+            InstallationCandidate(
+                "simple",
+                "1.0",
+                Link('simple-1.0-1-py3-abi3-linux_x86_64.whl'),
+            ),
+            InstallationCandidate(
+                "simple",
+                '1.0',
+                Link('simple-1.0-2-py3-abi3-linux_i386.whl'),
+            ),
+            InstallationCandidate(
+                "simple",
+                '1.0',
+                Link('simple-1.0-2-py3-any-none.whl'),
+            ),
+            InstallationCandidate(
+                "simple",
+                '1.0',
+                Link('simple-1.0.tar.gz'),
+            ),
+        ]
+        valid_tags = [
+            Tag('py3', 'abi3', 'linux_x86_64'),
+            Tag('py3', 'abi3', 'linux_i386'),
+            Tag('py3', 'any', 'none'),
+        ]
+        evaluator = CandidateEvaluator(
+            'my-project', supported_tags=valid_tags, specifier=SpecifierSet(),
+        )
+        sort_key = evaluator._sort_key
+        results = sorted(links, key=sort_key, reverse=True)
+        results2 = sorted(reversed(links), key=sort_key, reverse=True)
+
+        assert links == results, results
+        assert links == results2, results2
 
 
 def test_finder_priority_file_over_page(data):