Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/uv-pep440/src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ impl Version {
///
/// When the iterator yields no elements.
#[inline]
pub fn new<I, R>(release_numbers: I) -> Self
pub fn new<I, R>(release_segments: I) -> Self
where
I: IntoIterator<Item = R>,
R: Borrow<u64>,
Expand All @@ -302,7 +302,7 @@ impl Version {
small: VersionSmall::new(),
},
}
.with_release(release_numbers)
.with_release(release_segments)
}

/// Whether this is an alpha/beta/rc or dev version
Expand Down
86 changes: 72 additions & 14 deletions crates/uv-pep508/src/marker/algebra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

use std::cmp::Ordering;
use std::fmt;
use std::hash::Hash;
use std::ops::Bound;
use std::sync::{LazyLock, Mutex, MutexGuard};

Expand Down Expand Up @@ -183,6 +184,55 @@ impl InternerGuard<'_> {
}
}
},
// `<version key> ~= python_version` and `<version key> ~= python_full_version`
MarkerExpression::VersionInvertedTilde { key, specifier } => match key {
MarkerValueVersion::ImplementationVersion => {
// The number of segments in the implementation version is unknown and may
// only be a single one not support with tilde equal
return NodeId::FALSE;
}
MarkerValueVersion::PythonFullVersion => {
// Given `3.10 ~= python_full_version`,
// which matches for 3.10.0, 3.10.1, ... it becomes
// `python_full_version < 3.11 and python_version >= 3.10`
let major = specifier.version().release().first().copied().unwrap_or(0);
let minor = specifier.version().release().get(1).copied().unwrap_or(0);

let upper_bound =
VersionSpecifier::less_than_version(Version::new([major, minor + 1, 0]));
let lower_bound = if minor == 0 {
VersionSpecifier::greater_than_equal_version(Version::new([major]))
} else {
VersionSpecifier::greater_than_equal_version(Version::new([major, minor]))
};
let ranges = release_specifier_to_range(upper_bound, true)
.intersection(&release_specifier_to_range(lower_bound, true));
(
Variable::Version(CanonicalMarkerValueVersion::PythonFullVersion),
Edges::from_version_ranges(&ranges),
)
}
// Normalize `python_version` markers to `python_full_version` nodes.
MarkerValueVersion::PythonVersion => {
// Given `3.10 ~= python_version`,
// which matches for 3.0, 3.1, ..., 3.9, 3.10, it becomes
// `python_full_version < 3.11 and python_version >= 3`
let major = specifier.version().release().first().copied().unwrap_or(0);
let minor = specifier.version().release().get(1).copied().unwrap_or(0);

let upper_bound =
VersionSpecifier::less_than_version(Version::new([major, minor + 1]));
let lower_bound =
VersionSpecifier::greater_than_equal_version(Version::new([major]));
let ranges = release_specifier_to_range(upper_bound, true)
.intersection(&release_specifier_to_range(lower_bound, true));

(
Variable::Version(CanonicalMarkerValueVersion::PythonFullVersion),
Edges::from_version_ranges(&ranges),
)
}
},
// A variable representing the output of a version key. Edges correspond
// to disjoint version ranges.
MarkerExpression::VersionIn {
Expand Down Expand Up @@ -707,7 +757,7 @@ impl InternerGuard<'_> {
if matches!(i, NodeId::TRUE) {
let var = Variable::Version(CanonicalMarkerValueVersion::PythonFullVersion);
let edges = Edges::Version {
edges: Edges::from_range(&py_range),
edges: Edges::from_ranges(&py_range),
};
return self.create_node(var, edges).negate(i);
}
Expand Down Expand Up @@ -1220,15 +1270,21 @@ impl Edges {
};

Edges::String {
edges: Edges::from_range(&range),
edges: Edges::from_ranges(&range),
}
}

/// Returns the [`Edges`] for a version specifier.
fn from_specifier(specifier: VersionSpecifier) -> Edges {
let specifier = release_specifier_to_range(specifier.only_release(), true);
Edges::Version {
edges: Edges::from_range(&specifier),
edges: Edges::from_ranges(&specifier),
}
}

fn from_version_ranges(ranges: &Ranges<Version>) -> Edges {
Edges::Version {
edges: Edges::from_ranges(ranges),
}
}

Expand All @@ -1254,7 +1310,7 @@ impl Edges {
}

Ok(Edges::Version {
edges: Edges::from_range(&range),
edges: Edges::from_ranges(&range),
})
}

Expand All @@ -1275,25 +1331,25 @@ impl Edges {
}

Edges::Version {
edges: Edges::from_range(&range),
edges: Edges::from_ranges(&range),
}
}

/// Returns an [`Edges`] where values in the given range are `true`.
fn from_range<T>(range: &Ranges<T>) -> SmallVec<(Ranges<T>, NodeId)>
fn from_ranges<T>(ranges: &Ranges<T>) -> SmallVec<(Ranges<T>, NodeId)>
where
T: Ord + Clone,
{
let mut edges = SmallVec::new();

// Add the `true` edges.
for (start, end) in range.iter() {
for (start, end) in ranges.iter() {
let range = Ranges::from_range_bounds((start.clone(), end.clone()));
edges.push((range, NodeId::TRUE));
}

// Add the `false` edges.
for (start, end) in range.complement().iter() {
for (start, end) in ranges.complement().iter() {
let range = Ranges::from_range_bounds((start.clone(), end.clone()));
edges.push((range, NodeId::FALSE));
}
Expand Down Expand Up @@ -1610,10 +1666,10 @@ fn python_version_to_full_version(specifier: VersionSpecifier) -> Result<Version
// result in a `python_version` marker of `3.7`. For this reason, we must consider the range
// of values that would satisfy a `python_version` specifier when truncated in order to transform
// the specifier into its `python_full_version` equivalent.
if let Some((major, minor)) = major_minor {
let specifier = if let Some((major, minor)) = major_minor {
let version = Version::new([major, minor]);

Ok(match specifier.operator() {
match specifier.operator() {
// `python_version == 3.7` is equivalent to `python_full_version == 3.7.*`.
Operator::Equal | Operator::ExactEqual => {
VersionSpecifier::equals_star_version(version)
Expand All @@ -1638,13 +1694,13 @@ fn python_version_to_full_version(specifier: VersionSpecifier) -> Result<Version
// Handled above.
unreachable!()
}
})
}
} else {
let [major, minor, ..] = *specifier.version().release() else {
unreachable!()
};

Ok(match specifier.operator() {
match specifier.operator() {
// `python_version` cannot have more than two release segments, and we know
// that the following release segments aren't purely zeroes so equality is impossible.
Operator::Equal | Operator::ExactEqual => {
Expand All @@ -1668,8 +1724,10 @@ fn python_version_to_full_version(specifier: VersionSpecifier) -> Result<Version
// Handled above.
unreachable!()
}
})
}
}
};

Ok(specifier)
}

/// Compares the start of two ranges that are known to be disjoint.
Expand Down
48 changes: 39 additions & 9 deletions crates/uv-pep508/src/marker/parse.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use arcstr::ArcStr;
use std::str::FromStr;
use uv_normalize::{ExtraName, GroupName};
use uv_pep440::{Version, VersionPattern, VersionSpecifier};
use uv_pep440::{Operator, Version, VersionPattern, VersionSpecifier};

use crate::cursor::Cursor;
use crate::marker::MarkerValueExtra;
Expand Down Expand Up @@ -282,12 +282,26 @@ pub(crate) fn parse_marker_key_op_value<T: Pep508Url>(
parse_inverted_version_expr(&l_string, operator, key, reporter)
}
// '...' == <env key>
MarkerValue::MarkerEnvString(key) => Some(MarkerExpression::String {
key,
MarkerValue::MarkerEnvString(key) => {
// Invert the operator to normalize the expression order.
operator: operator.invert(),
value: l_string,
}),
if let Some(operator) = operator.invert() {
Some(MarkerExpression::String {
key,
operator,
value: l_string,
})
} else {
debug_assert_eq!(operator, MarkerOperator::TildeEqual);
reporter.report(
MarkerWarningKind::StringStringComparison,
format!(
"Comparing a string with `~=` doesn't make sense:
'{l_string}' {operator} {r_value}, will be ignored"
),
);
None
}
}
// `"test" in extras` or `"dev" in dependency_groups`
MarkerValue::MarkerEnvList(key) => {
let operator =
Expand Down Expand Up @@ -484,9 +498,6 @@ fn parse_inverted_version_expr(
key: MarkerValueVersion,
reporter: &mut impl Reporter,
) -> Option<MarkerExpression> {
// Invert the operator to normalize the expression order.
let marker_operator = marker_operator.invert();

// Not star allowed here, `'3.*' == python_version` is not a valid PEP 440 comparison.
let version = match value.parse::<Version>() {
Ok(version) => version,
Expand All @@ -503,6 +514,25 @@ fn parse_inverted_version_expr(
}
};

// Invert the operator to normalize the expression order.
let Some(marker_operator) = marker_operator.invert() else {
// The only operator that can't be inverted is `~=`.
debug_assert_eq!(marker_operator, MarkerOperator::TildeEqual);
return Some(MarkerExpression::VersionInvertedTilde {
key,
specifier: match VersionSpecifier::from_version(Operator::TildeEqual, version) {
Ok(specifier) => specifier,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!("Invalid operator/version combination: {err}"),
);
return None;
}
},
});
};

let Some(operator) = marker_operator.to_pep440_operator() else {
reporter.report(
MarkerWarningKind::Pep440Error,
Expand Down
4 changes: 4 additions & 0 deletions crates/uv-pep508/src/marker/simplify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ fn is_negation(left: &MarkerExpression, right: &MarkerExpression) -> bool {
.negate()
.is_some_and(|negated| negated == *specifier2.operator())
}
MarkerExpression::VersionInvertedTilde { .. } => {
// The inversion is not a single expression.
false
}
MarkerExpression::VersionIn {
key,
versions,
Expand Down
Loading
Loading