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:
committed by
Andrew Gallant
parent
94a0a0f2ee
commit
4ff057e108
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user