-
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
Refactor incompatiblity tracking for distributions #1298
Conversation
0d6303b
to
7251583
Compare
90afb5e
to
b5da4ff
Compare
7251583
to
3fe8381
Compare
cfbe825
to
dfa4a5e
Compare
Instead of dropping versions without a compatible distribution, we track them as incompatibilities in the solver. This implementation follows patterns established in #1290. This required some significant refactoring of how we track incompatible distributions. Notably: - `Option<TagPriority>` is now `WheelCompatibility` which allows us to track the reason a wheel is incompatible instead of just `None`. - `Candidate` now has a `CandidateDist` with `Compatible` and `Incompatibile` variants instead of just `ResolvableDist`; candidates are not strictly compatible anymore - `ResolvableDist` was renamed to `CompatibleDist` - `IncompatibleWheel` was given an ordering implementation so we can track the "most compatible" (but still incompatible) wheel. This allows us to collapse the reason a version cannot be used to a single incompatibility. - The filtering in the `VersionMap` is retained, we still only store one incompatible wheel per version. This is sufficient for error reporting. - A `TagCompatibility` type was added for tracking which part of a wheel tag is incompatible - `Candidate::validate_python` moved to `PythonRequirement::validate_dist` I am doing more refactoring in #1298 — I think a couple passes will be necessary to clarify the relationships of these types. Includes improved error message snapshots for multiple incompatible Python tag types from #1285 — we should add more scenarios for coverage of behavior when multiple tags with different levels are present.
This comment was marked as outdated.
This comment was marked as outdated.
a7ea828
to
cb36cf3
Compare
} | ||
IncompatibleWheel::RequiresPython(_) => unreachable!(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can RequiresPython
be refactored to avoid this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could consider putting this whole block in here instead of using the continue
and the unreachable!()
:
let python_version = requires_python
.iter()
.map(PubGrubSpecifier::try_from)
.fold_ok(Range::full(), |range, specifier| {
range.intersection(&specifier.into())
})?;
let package = &next;
for kind in [PubGrubPython::Installed, PubGrubPython::Target] {
state.add_incompatibility(Incompatibility::from_dependency(
package.clone(),
Range::singleton(version.clone()),
(PubGrubPackage::Python(kind), python_version.clone()),
));
}
state.partial_solution.add_decision(next.clone(), version);
continue;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then we need to write that whole thing twice which I find less ideal.
My concern is less about the unreachable!
and more that it suggests a flaw in the abstraction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the abstraction is awkward, but I guess what I was pointing out (which maybe isn't useful) is that to the degree that the unreachable!
itself represents a flaw in the abstraction, that's just a consequence of how the code is structured locally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 yeah that makes sense. This would be partially alleviated by a restructuring to avoid duplication as described in #1298 (comment)
ExcludeNewer(Option<i64>), | ||
RequiresPython(VersionSpecifiers), | ||
Yanked(Yanked), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These three overlap with variants in IncompatibleWheel
. Should we combine them into a structure like:
Incompatible
ExcludeNewer
RequiresPython
Yanked
Wheel
NoBinary
Tag
SourceDist
NoBuild
1701d64
to
5435865
Compare
I think this can be improved further, but now that it includes the user-facing improvement from #2066 I think we may want to bias towards merging. Happy for any suggestions on different structuring. |
# Conflicts: # crates/distribution-types/src/prioritized_distribution.rs # crates/puffin-resolver/src/candidate_selector.rs # crates/puffin-resolver/src/version_map.rs # Conflicts: # crates/distribution-types/src/prioritized_distribution.rs # crates/uv-client/src/flat_index.rs # crates/uv-resolver/src/candidate_selector.rs # crates/uv-resolver/src/finder.rs # crates/uv-resolver/src/python_requirement.rs # crates/uv-resolver/src/resolver/mod.rs # crates/uv-resolver/src/resolver/provider.rs
Supersedes #2066 # Conflicts: # crates/uv-resolver/src/candidate_selector.rs
5435865
to
aaa874e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a big improvement.
@@ -300,3 +285,64 @@ impl From<TagCompatibility> for WheelCompatibility { | |||
} | |||
} | |||
} | |||
|
|||
impl IncompatibleSource { | |||
fn is_more_compatible(&self, other: &IncompatibleSource) -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not Ord
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my branch just made everything Ord
and then I had, like, compatibility > existing
or something. But maybe it ends up being more complicated, the logic in here is quite nuanced. (This isn't blocking feedback.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Satisfying Ord
is actually really challenging. For example, when comparing RequiresPython
incompatibilities that contain version specifiers we cannot really provide strong ordering. I attempted to do Ord
at first as well and after much discussion @BurntSushi recommended this approach instead.
I believe the reason this worked on your branch is that you didn't retain information on the incompatibility enum members. Enum members themselves are trivially ordered, but once we store additional metadata about the incompatibility in them we need ordering in a larger hierarchy of types.
If you have other ideas to get around this please share :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separately.. the nuance here is a little frightening to me. Selecting the "most relevant incompatible distribution" feels like it could be brittle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's no clear precedence, can with just stringifying the incompatibility criteria and sort based on that? It's arbitrary yet deterministic and easy to follow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's actually what I did to implement Ord
originally, but it's hard to be sure we're meeting all of the requirements for the trait and it seems brittle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what you have here is totally fine.
} | ||
IncompatibleWheel::RequiresPython(_) => unreachable!(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could consider putting this whole block in here instead of using the continue
and the unreachable!()
:
let python_version = requires_python
.iter()
.map(PubGrubSpecifier::try_from)
.fold_ok(Range::full(), |range, specifier| {
range.intersection(&specifier.into())
})?;
let package = &next;
for kind in [PubGrubPython::Installed, PubGrubPython::Target] {
state.add_incompatibility(Incompatibility::from_dependency(
package.clone(),
Range::singleton(version.clone()),
(PubGrubPackage::Python(kind), python_version.clone()),
));
}
state.partial_solution.add_decision(next.clone(), version);
continue;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a big improvement for the error messages
@@ -300,3 +285,64 @@ impl From<TagCompatibility> for WheelCompatibility { | |||
} | |||
} | |||
} | |||
|
|||
impl IncompatibleSource { | |||
fn is_more_compatible(&self, other: &IncompatibleSource) -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's no clear precedence, can with just stringifying the incompatibility criteria and sort based on that? It's arbitrary yet deterministic and easy to follow.
/// Here, we track if each file is yanked separately. If a release is partially yanked, the | ||
/// unyanked distributions _can_ be used. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hope no index implements this, this would be quite confusing to users
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fun fact, the spec is about marking a particular file as yanked: https://peps.python.org/pep-0592/.
PyPI has implemented it as only being able to yank an entire release, but that is a PyPI implementation detail.
This has come up a few times on discussing particular behavior pip could adopt around yanking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see why it needs to be like that on the API level (there are no releases in the API, just files), but i'd strongly prefer it if indexes made this an all or nothing operation per release.
While this PEP implements yanking at the file level, that is largely due to the shape the simple repository API takes, not a specific decision made by this PEP.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, there has been some recent discussion about requiring behavior on indexes, not just the API, and the concern was being able to find and get buy in from non PyPI indexes (Azure, Gitlab, Artifactory, etc.). Perhaps a good case to put forth as a trial to see if it's possible.
Extends the "compatibility" types introduced in #1293 to apply to source distributions as well as wheels.
DistMetadata
!)