Repository: pantsbuild/pants

Issue:
`InterpreterConstraints` constructor does not always preserve sorted order correctly
This test should pass, but it doesn't:

```python
def test_ic_ordering():
    inputs = ["CPython==2.7.*", "PyPy", "CPython>=3.6,<4,!=3.7.*"]
    a = InterpreterConstraints(inputs)
    a_str = [str(i) for i in a]
    b = InterpreterConstraints(a_str)

    print(inputs)
    print(a)
    print(a_str)
    print(b) 

    assert a == b
```

This produces stdout:

```
['CPython==2.7.*', 'PyPy', 'CPython>=3.6,<4,!=3.7.*']
CPython==2.7.* OR CPython!=3.7.*,<4,>=3.6 OR PyPy
['CPython==2.7.*', 'CPython!=3.7.*,<4,>=3.6', 'PyPy']
CPython!=3.7.*,<4,>=3.6 OR CPython==2.7.* OR PyPy
```



Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/python/util_rules/interpreter_constraints.py b/src/python/pants/backend/python/util_rules/interpreter_constraints.py
--- a/src/python/pants/backend/python/util_rules/interpreter_constraints.py
+++ b/src/python/pants/backend/python/util_rules/interpreter_constraints.py
@@ -42,10 +42,13 @@ def interpreter_constraints(self) -> InterpreterConstraintsField:
 # Normally we would subclass `DeduplicatedCollection`, but we want a custom constructor.
 class InterpreterConstraints(FrozenOrderedSet[Requirement], EngineAwareParameter):
     def __init__(self, constraints: Iterable[str | Requirement] = ()) -> None:
-        super().__init__(
-            v if isinstance(v, Requirement) else self.parse_constraint(v)
-            for v in sorted(constraints, key=lambda c: str(c))
+        # #12578 `parse_constraint` will sort the requirement's component constraints into a stable form.
+        # We need to sort the component constraints for each requirement _before_ sorting the entire list
+        # for the ordering to be correct.
+        parsed_constraints = (
+            i if isinstance(i, Requirement) else self.parse_constraint(i) for i in constraints
         )
+        super().__init__(sorted(parsed_constraints, key=lambda c: str(c)))
 
     def __str__(self) -> str:
         return " OR ".join(str(constraint) for constraint in self)


Test patch:
diff --git a/src/python/pants/backend/python/util_rules/interpreter_constraints_test.py b/src/python/pants/backend/python/util_rules/interpreter_constraints_test.py
--- a/src/python/pants/backend/python/util_rules/interpreter_constraints_test.py
+++ b/src/python/pants/backend/python/util_rules/interpreter_constraints_test.py
@@ -293,7 +293,7 @@ def test_group_field_sets_by_constraints_with_unsorted_inputs() -> None:
     "constraints,expected",
     (
         (["==2.7.*"], "==2.7.*"),
-        (["==2.7.*", ">=3.6,!=3.6.1"], "==2.7.* || !=3.6.1,>=3.6"),
+        (["==2.7.*", ">=3.6,!=3.6.1"], "!=3.6.1,>=3.6 || ==2.7.*"),
         ([], "*"),
         # If any of the constraints are unconstrained (e.g. `CPython`), use a wildcard.
         (["==2.7", ""], "*"),
@@ -398,3 +398,14 @@ def test_contains(candidate, target, matches) -> None:
         )
         == matches
     )
+
+
+def test_constraints_are_correctly_sorted_at_construction() -> None:
+    # #12578: This list itself is out of order, and `CPython>=3.6,<4,!=3.7.*` is specified with
+    # out-of-order component requirements. This test verifies that the list is fully sorted after
+    # the first call to `InterpreterConstraints()`
+    inputs = ["CPython==2.7.*", "PyPy", "CPython>=3.6,<4,!=3.7.*"]
+    a = InterpreterConstraints(inputs)
+    a_str = [str(i) for i in a]
+    b = InterpreterConstraints(a_str)
+    assert a == b