-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Local version identifiers are not ignored when testing version equality #1855
Comments
Thanks! @BurntSushi - do you mind taking a look? |
My first thought was something like: --- a/crates/pep440-rs/src/version.rs
+++ b/crates/pep440-rs/src/version.rs
@@ -564,7 +564,19 @@ impl Version {
}
// release is equal, so compare the other parts
- sortable_tuple(self).cmp(&sortable_tuple(other))
+ match sortable_tuple(self).cmp(&sortable_tuple(other)) {
+ Ordering::Less => return Ordering::Less,
+ Ordering::Equal => {},
+ Ordering::Greater => return Ordering::Greater,
+ }
+
+ // post- and pre-releases are equal, so compare the local
+ match (self.local(), other.local()) {
+ ([], []) => Ordering::Equal,
+ ([], _) => Ordering::Equal,
+ (_, []) => Ordering::Equal,
+ (a, b) => a.cmp(b),
+ }
}
}
@@ -2203,7 +2215,7 @@ pub(crate) fn compare_release(this: &[u64], other: &[u64]) -> Ordering {
/// aN, bN, rcN, <no suffix (final)>, .postN but also, you can have dev/post
/// releases on beta releases, so we make a three stage ordering: ({dev: 0, a:
/// 1, b: 2, rc: 3, (): 4, post: 5}, <preN>, <postN or None as smallest>, <devN
-/// or Max as largest>, <local>)
+/// or Max as largest>)
///
/// For post, any number is better than none (so None defaults to None<0),
/// but for dev, no number is better (so None default to the maximum). For
@@ -2211,10 +2223,10 @@ pub(crate) fn compare_release(this: &[u64], other: &[u64]) -> Ordering {
/// implementation
///
/// [pep440-suffix-ordering]: https://peps.python.org/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering
-fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegment]) {
+fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64) {
match (version.pre(), version.post(), version.dev()) {
// dev release
- (None, None, Some(n)) => (0, 0, None, n, version.local()),
+ (None, None, Some(n)) => (0, 0, None, n),
// alpha release
(
Some(PreRelease {
@@ -2223,7 +2235,7 @@ fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegm
}),
post,
dev,
- ) => (1, n, post, dev.unwrap_or(u64::MAX), version.local()),
+ ) => (1, n, post, dev.unwrap_or(u64::MAX)),
// beta release
(
Some(PreRelease {
@@ -2232,7 +2244,7 @@ fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegm
}),
post,
dev,
- ) => (2, n, post, dev.unwrap_or(u64::MAX), version.local()),
+ ) => (2, n, post, dev.unwrap_or(u64::MAX)),
// alpha release
(
Some(PreRelease {
@@ -2241,11 +2253,11 @@ fn sortable_tuple(version: &Version) -> (u64, u64, Option<u64>, u64, &[LocalSegm
}),
post,
dev,
- ) => (3, n, post, dev.unwrap_or(u64::MAX), version.local()),
+ ) => (3, n, post, dev.unwrap_or(u64::MAX)),
// final release
- (None, None, None) => (4, 0, None, 0, version.local()),
+ (None, None, None) => (4, 0, None, 0),
// post release
- (None, Some(post), dev) => (5, 0, Some(post), dev.unwrap_or(u64::MAX), version.local()),
+ (None, Some(post), dev) => (5, 0, Some(post), dev.unwrap_or(u64::MAX)),
}
} |
That seems reasonable but I'm hitting an error in the lazy version map, maybe something to do with equal comparators but different hashes...? |
PubGrub requires a "root" package so |
Only guessing, but I guess that proposed fix is too symmetrical
|
One temporary workaround using overrides: #1497 (comment) |
We don't have test coverage for this, but a term can reference an incompatibility with root and then we'll display the internal 'root' package to the user. Raised in #1855
OK, so I've been looking into this recently, and after a ~day of yak-shaving to get tests working, I've had a chance to dig in a bit more. I was specifically digging into this in the context of #1497, but I believe that issue and this one are at least connected and likely share the same underlying problem. Indeed, as @dimbleby put it:
Today, the latter is true in This is somewhat subtle, because in our PEP 440 crate, we do actually handle this case correctly: uv/crates/pep440-rs/src/version_specifier.rs Lines 413 to 424 in 20253cd
But in our resolver, that With all that said, I did try a couple things to see what happens. The first thing I tried was to modify candidate selection: uv/crates/uv-resolver/src/candidate_selector.rs Lines 169 to 176 in 20253cd
I modified it by changing the candidate version to always have its local segment stripped. (This is not fully correct since sometimes we do want to compare the local segments, specifically, when the version constraint itself has a local segment in it. But it does fix #1497.) So that means if we find a candidate with version The second modification I made (which also fixes #1497) was also to candidate selection. In this case however, instead of returning a candidate with the local segment stripped, the candidate is always returned with its actual version, as it is in the status quo. Instead, all I did was change In discussion with @charliermarsh a couple days ago, he suggested taking an approach similar to pre-releases where we allow local versions of packages only if the version specifier itself also has a local segment. This would mean that something like However, one key thing with @charliermarsh's approach is that because of the existence of the constraint It almost seems like what we want is some kind of version-wrapper type that permits ignoring local segments for For now, I'm going to clean up my |
I'm going to pick this back up, wish me luck. |
## Summary This PR adds limited support for PEP 440-compatible local version testing. Our behavior is _not_ comprehensively in-line with the spec. However, it does fix by _far_ the biggest practical limitation, and resolves all the issues that've been raised on uv related to local versions without introducing much complexity into the resolver, so it feels like a good tradeoff for me. I'll summarize the change here, but for more context, see [Andrew's write-up](#1855 (comment)) in the linked issue. Local version identifiers are really tricky because of asymmetry. `==1.2.3` should allow `1.2.3+foo`, but `==1.2.3+foo` should not allow `1.2.3`. It's very hard to map them to PubGrub, because PubGrub doesn't think of things in terms of individual specifiers (unlike the PEP 440 spec) -- it only thinks in terms of ranges. Right now, resolving PyTorch and friends fails, because... - The user provides requirements like `torch==2.0.0+cu118` and `torchvision==0.15.1+cu118`. - We then match those exact versions. - We then look at the requirements of `torchvision==0.15.1+cu118`, which includes `torch==2.0.0`. - Under PEP 440, this is fine, because `torch @ 2.0.0+cu118` should be compatible with `torch==2.0.0`. - In our model, though, it's not, because these are different versions. If we change our comparison logic in various places to allow this, we risk breaking some fundamental assumptions of PubGrub around version continuity. - Thus, we fail to resolve, because we can't accept both `torch @ 2.0.0` and `torch @ 2.0.0+cu118`. As compared to the solutions we explored in #1855 (comment), at a high level, this approach differs in that we lie about the _dependencies_ of packages that rely on our local-version-using package, rather than lying about the versions that exist, or the version we're returning, etc. In short: - When users specify local versions upfront, we keep track of them. So, above, we'd take note of `torch` and `torchvision`. - When we convert the dependencies of a package to PubGrub ranges, we check if the requirement matches `torch` or `torchvision`. If it's an`==`, we check if it matches (in the above example) for `torch==2.0.0`. If so, we _change_ the requirement to `torch==2.0.0+cu118`. (If it's `==` some other version, we return an incompatibility.) In other words, we selectively override the declared dependencies by making them _more specific_ if a compatible local version was specified upfront. The net effect here is that the motivating PyTorch resolutions all work. And, in general, transitive local versions work as expected. The thing that still _doesn't_ work is: imagine if there were _only_ local versions of `torch` available. Like, `torch @ 2.0.0` didn't exist, but `torch @ 2.0.0+cpu` did, and `torch @ 2.0.0+gpu` did, and so on. `pip install torch==2.0.0` would arbitrarily choose one one `2.0.0+cpu` or `2.0.0+gpu`, and that's correct as per PEP 440 (local version segments should be completely ignored on `torch==2.0.0`). However, uv would fail to identify a compatible version. I'd _probably_ prefer to fix this, although candidly I think our behavior is _ok_ in practice, and it's never been reported as an issue. Closes #1855. Closes #2080. Closes #2328.
This will be supported in the next release via #2430. |
Platform: Ubuntu 22.04
uv
version: 0.1.6 (da3a7ec)Spill-over from #1853, while checking that
--editable
gave the intended behavior on that ticket, I discovered thatuv
seems to be including local version identifiers of candidates when considering a requirement that has no local version identifierThe PyPA
Version Specifiers
standard (and PEP 440 before it) states that:My understanding of that passage is that
torchvision
depends ontorch==2.0.0
(i.e. it declares its dependency against a public version identifier), so the equality comparison MUST ignore any local version identifier (like+cu118
) when considering candidate versions oftorch
.As I understand it, the current behavior of
uv
would be correct iftorchvision
declared a dependency ontorch==2.0.0+…
that had a local segment that was not exactly+cu118
, but it's doing the wrong thing for the public version identifier.(As an aside, I'm not sure what
root==0a0.dev0
is about? Perhaps some kind of meta-requirement associated with my entire request? I don't think it's related to the failure here, but I do find it confusing in the report)The text was updated successfully, but these errors were encountered: