uv-resolver: refactor how we deal with requires-python

This commit refactors how deal with `requires-python` so that instead of
simplifying markers of dependencies inside the resolver, we do it at the
edges of our system. When writing markers to output, we simplify when
there's an obvious `requires-python` context. And when reading markers
as input, we complexity markers with the relevant `requires-python`
constraint.
This commit is contained in:
Andrew Gallant 2024-08-19 18:05:01 -04:00 committed by Andrew Gallant
parent 94a0a0f2ee
commit 4ff057e108
12 changed files with 544 additions and 164 deletions

View file

@ -125,8 +125,19 @@ impl RequiresPython {
}
}
/// Returns the [`RequiresPython`] as a [`MarkerTree`].
pub fn markers(&self) -> MarkerTree {
/// Returns this `Requires-Python` specifier as an equivalent
/// [`MarkerTree`] utilizing the `python_full_version` marker field.
///
/// This is useful for comparing a `Requires-Python` specifier with
/// arbitrary marker expressions. For example, one can ask whether the
/// returned marker expression is disjoint with another marker expression.
/// If it is, then one can conclude that the `Requires-Python` specifier
/// excludes the dependency with that other marker expression.
///
/// If this `Requires-Python` specifier has no constraints, then this
/// returns a marker tree that evaluates to `true` for all possible marker
/// environments.
pub fn to_marker_tree(&self) -> MarkerTree {
match (self.range.0.as_ref(), self.range.1.as_ref()) {
(Bound::Included(lower), Bound::Included(upper)) => {
let mut lower = MarkerTree::expression(MarkerExpression::Version {
@ -283,6 +294,69 @@ impl RequiresPython {
&self.range
}
/// Simplifies the given markers in such a way as to assume that
/// the Python version is constrained by this Python version bound.
///
/// For example, with `requires-python = '>=3.8'`, a marker like this:
///
/// ```text
/// python_full_version >= '3.8' and python_full_version < '3.12'
/// ```
///
/// Will be simplified to:
///
/// ```text
/// python_full_version < '3.12'
/// ```
///
/// That is, `python_full_version >= '3.8'` is assumed to be true by virtue
/// of `requires-python`, and is thus not needed in the marker.
///
/// This should be used in contexts in which this assumption is valid to
/// make. Generally, this means it should not be used inside the resolver,
/// but instead near the boundaries of the system (like formatting error
/// messages and writing the lock file). The reason for this is that
/// this simplification fundamentally changes the meaning of the marker,
/// and the *only* correct way to interpret it is in a context in which
/// `requires-python` is known to be true. For example, when markers from
/// a lock file are deserialized and turned into a `ResolutionGraph`, the
/// markers are "complexified" to put the `requires-python` assumption back
/// into the marker explicitly.
pub(crate) fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree {
let simplified = marker.simplify_python_versions(Range::from(self.range().clone()));
// FIXME: This is a hack to avoid the hidden state created by
// ADD's `restrict_versions`. I believe this is sound, but it's
// wasteful and silly.
simplified
.try_to_string()
.map(|s| s.parse().unwrap())
.unwrap_or(MarkerTree::TRUE)
}
/// The inverse of `simplify_markers`.
///
/// This should be applied near the boundaries of uv when markers are
/// deserialized from a context where `requires-python` is assumed. For
/// example, with `requires-python = '>=3.8'` and a marker like:
///
/// ```text
/// python_full_version < '3.12'
/// ```
///
/// It will be "complexified" to:
///
/// ```text
/// python_full_version >= '3.8' and python_full_version < '3.12'
/// ```
pub(crate) fn complexify_markers(&self, mut marker: MarkerTree) -> MarkerTree {
// PERF: There's likely a way to amortize this, particularly
// the construction of `to_marker_tree`. But at time of
// writing, it wasn't clear if this was an actual perf problem
// or not. If it is, try a `std::sync::OnceLock`.
marker.and(self.to_marker_tree());
marker
}
/// Returns `false` if the wheel's tags state it can't be used in the given Python version
/// range.
///
@ -477,6 +551,46 @@ impl Ord for RequiresPythonBound {
}
}
/// A simplified marker is just like a normal marker, except it has possibly
/// been simplified by `requires-python`.
///
/// A simplified marker should only exist in contexts where a `requires-python`
/// setting can be assumed. In order to get a "normal" marker out of
/// a simplified marker, one must re-contextualize it by adding the
/// `requires-python` constraint back to the marker.
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
pub(crate) struct SimplifiedMarkerTree(MarkerTree);
impl SimplifiedMarkerTree {
/// Simplifies the given markers by assuming the given `requires-python`
/// bound is true.
pub(crate) fn new(
requires_python: &RequiresPython,
marker: MarkerTree,
) -> SimplifiedMarkerTree {
SimplifiedMarkerTree(requires_python.simplify_markers(marker))
}
/// Complexifies the given markers by adding the given `requires-python` as
/// a constraint to these simplified markers.
pub(crate) fn into_marker(self, requires_python: &RequiresPython) -> MarkerTree {
requires_python.complexify_markers(self.0)
}
/// Attempts to convert this simplified marker to a string.
///
/// This only returns `None` when the underlying marker is always true,
/// i.e., it matches all possible marker environments.
pub(crate) fn try_to_string(&self) -> Option<String> {
self.0.try_to_string()
}
/// Returns the underlying marker tree without re-complexifying them.
pub(crate) fn as_simplified_marker_tree(&self) -> &MarkerTree {
&self.0
}
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;