Prioritize forks based on upper bounds (#5643)

## Summary

Given a fork like:

```
pylint < 3 ; sys_platform == 'darwin'
pylint > 2 ; sys_platform != 'darwin'
```

Solving the top branch will typically yield a solution that also
satisfies the bottom branch, due to maximum version selection (while the
inverse isn't true).

To quote an example from the docs:

```rust
// If there's no difference, prioritize forks with upper bounds. We'd prefer to solve
// `numpy <= 2` before solving `numpy >= 1`, since the resolution produced by the former
// might work for the latter, but the inverse is unlikely to be true due to maximum
// version selection. (Selecting `numpy==2.0.0` would satisfy both forks, but selecting
// the latest `numpy` would not.)
```

Closes https://github.com/astral-sh/uv/issues/4926 for now.
This commit is contained in:
Charlie Marsh 2024-07-31 11:05:12 -04:00 committed by GitHub
parent 89947681d1
commit c2a6cb391b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 85 additions and 1 deletions

View file

@ -2844,7 +2844,34 @@ impl Ord for Fork {
// work for the latter, but the inverse is unlikely to be true.
let self_bound = requires_python_marker(&self.markers).unwrap_or_default();
let other_bound = requires_python_marker(&other.markers).unwrap_or_default();
other_bound.cmp(&self_bound)
other_bound.cmp(&self_bound).then_with(|| {
// If there's no difference, prioritize forks with upper bounds. We'd prefer to solve
// `numpy <= 2` before solving `numpy >= 1`, since the resolution produced by the former
// might work for the latter, but the inverse is unlikely to be true due to maximum
// version selection. (Selecting `numpy==2.0.0` would satisfy both forks, but selecting
// the latest `numpy` would not.)
let self_upper_bounds = self
.dependencies
.iter()
.filter(|dep| {
dep.version
.bounding_range()
.is_some_and(|(_, upper)| !matches!(upper, Bound::Unbounded))
})
.count();
let other_upper_bounds = other
.dependencies
.iter()
.filter(|dep| {
dep.version
.bounding_range()
.is_some_and(|(_, upper)| !matches!(upper, Bound::Unbounded))
})
.count();
self_upper_bounds.cmp(&other_upper_bounds)
})
}
}