From 4a90bef83e075d1dc86108a58ff3a11d1467e41c Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 21 Jul 2025 18:50:24 +0200 Subject: [PATCH] Add variant support to uv-pep508 --- crates/uv-pep508/src/lib.rs | 20 +- crates/uv-pep508/src/marker/lowering.rs | 19 +- crates/uv-pep508/src/marker/parse.rs | 54 +++- crates/uv-pep508/src/marker/tree.rs | 323 +++++++++++++++++++----- crates/uv-pep508/src/unnamed.rs | 13 +- 5 files changed, 360 insertions(+), 69 deletions(-) diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index 30c7b9362..d332ade3f 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -82,6 +82,17 @@ pub enum Pep508ErrorSource { /// The version requirement is not supported. #[error("{0}")] UnsupportedRequirement(String), + /// The operator is not supported with the variant marker. + #[error( + "The operator {0} is not supported with the marker {1}, only the `in` and `not in` operators are supported" + )] + ListOperator(MarkerOperator, MarkerValueList), + /// The value is not a quoted string. + #[error("Only quoted strings are supported with the variant marker {1}, not {0}")] + ListValue(MarkerValue, MarkerValueList), + /// The variant marker is on the left hand side of the expression. + #[error("The marker {0} must be on the right hand side of the expression")] + ListLValue(MarkerValueList), } impl Display for Pep508Error { @@ -298,8 +309,13 @@ impl CacheKey for Requirement { impl Requirement { /// Returns whether the markers apply for the given environment - pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool { - self.marker.evaluate(env, extras) + pub fn evaluate_markers( + &self, + env: &MarkerEnvironment, + variants: Option<&[(String, String, String)]>, + extras: &[ExtraName], + ) -> bool { + self.marker.evaluate(env, variants, extras) } /// Return the requirement with an additional marker added, to require the given extra. diff --git a/crates/uv-pep508/src/marker/lowering.rs b/crates/uv-pep508/src/marker/lowering.rs index e52669840..95b2ef252 100644 --- a/crates/uv-pep508/src/marker/lowering.rs +++ b/crates/uv-pep508/src/marker/lowering.rs @@ -1,5 +1,4 @@ use std::fmt::{Display, Formatter}; - use uv_normalize::{ExtraName, GroupName}; use crate::marker::tree::MarkerValueList; @@ -163,13 +162,19 @@ impl Display for CanonicalMarkerValueExtra { /// A key-value pair for ` in ` or ` not in `, where the key is a list. /// -/// Used for PEP 751 markers. +/// Used for PEP 751 and variant markers. #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub enum CanonicalMarkerListPair { /// A valid [`ExtraName`]. Extras(ExtraName), /// A valid [`GroupName`]. DependencyGroup(GroupName), + /// A valid `variant_namespaces`. + VariantNamespaces(String), + /// A valid `variant_features`. + VariantFeatures(String, String), + /// A valid `variant_properties`. + VariantProperties(String, String, String), /// For leniency, preserve invalid values. Arbitrary { key: MarkerValueList, value: String }, } @@ -180,6 +185,9 @@ impl CanonicalMarkerListPair { match self { Self::Extras(_) => MarkerValueList::Extras, Self::DependencyGroup(_) => MarkerValueList::DependencyGroups, + Self::VariantNamespaces(_) => MarkerValueList::VariantNamespaces, + Self::VariantFeatures(_, _) => MarkerValueList::VariantFeatures, + Self::VariantProperties(_, _, _) => MarkerValueList::VariantProperties, Self::Arbitrary { key, .. } => *key, } } @@ -189,6 +197,13 @@ impl CanonicalMarkerListPair { match self { Self::Extras(extra) => extra.to_string(), Self::DependencyGroup(group) => group.to_string(), + Self::VariantNamespaces(namespace) => namespace.clone(), + Self::VariantFeatures(namespace, property) => { + format!("{namespace} :: {property}") + } + Self::VariantProperties(namespace, property, value) => { + format!("{namespace} :: {property} :: {value}") + } Self::Arbitrary { value, .. } => value.clone(), } } diff --git a/crates/uv-pep508/src/marker/parse.rs b/crates/uv-pep508/src/marker/parse.rs index 8e4a39078..50f159a53 100644 --- a/crates/uv-pep508/src/marker/parse.rs +++ b/crates/uv-pep508/src/marker/parse.rs @@ -181,6 +181,9 @@ pub(crate) fn parse_marker_key_op_value( let r_value = parse_marker_value(cursor, reporter)?; let len = cursor.pos() - start; + // TODO(konsti): Catch incorrect variant markers in all places, now that we have the + // opportunity to check from the beginning. + // Convert a ` ` expression into its // typed equivalent. let expr = match l_value { @@ -307,7 +310,7 @@ pub(crate) fn parse_marker_key_op_value( Ok(name) => CanonicalMarkerListPair::Extras(name), Err(err) => { reporter.report( - MarkerWarningKind::ExtrasInvalidComparison, + MarkerWarningKind::ListInvalidComparison, format!("Expected extra name (found `{l_string}`): {err}"), ); CanonicalMarkerListPair::Arbitrary { @@ -322,7 +325,7 @@ pub(crate) fn parse_marker_key_op_value( Ok(name) => CanonicalMarkerListPair::DependencyGroup(name), Err(err) => { reporter.report( - MarkerWarningKind::ExtrasInvalidComparison, + MarkerWarningKind::ListInvalidComparison, format!("Expected dependency group name (found `{l_string}`): {err}"), ); CanonicalMarkerListPair::Arbitrary { @@ -332,6 +335,53 @@ pub(crate) fn parse_marker_key_op_value( } } } + MarkerValueList::VariantNamespaces => { + // TODO(konsti): Validate + CanonicalMarkerListPair::VariantNamespaces(l_string.trim().to_string()) + } + MarkerValueList::VariantFeatures => { + if let Some((namespace, feature)) = l_string.split_once("::") { + // TODO(konsti): Validate + CanonicalMarkerListPair::VariantFeatures( + namespace.trim().to_string(), + feature.trim().to_string(), + ) + } else { + reporter.report( + MarkerWarningKind::ListInvalidComparison, + format!("Expected variant feature with two components seperated by `::`, found `{l_string}`"), + ); + CanonicalMarkerListPair::Arbitrary { + key, + value: l_string.to_string(), + } + } + } + MarkerValueList::VariantProperties => { + let mut components = l_string.split("::"); + if let (Some(namespace), Some(feature), Some(property), None) = ( + components.next(), + components.next(), + components.next(), + components.next(), + ) { + // TODO(konsti): Validate + CanonicalMarkerListPair::VariantProperties( + namespace.trim().to_string(), + feature.trim().to_string(), + property.trim().to_string(), + ) + } else { + reporter.report( + MarkerWarningKind::ListInvalidComparison, + format!("Expected variant property with three components seperated by `::`, found `{l_string}`"), + ); + CanonicalMarkerListPair::Arbitrary { + key, + value: l_string.to_string(), + } + } + } }; Some(MarkerExpression::List { pair, operator }) diff --git a/crates/uv-pep508/src/marker/tree.rs b/crates/uv-pep508/src/marker/tree.rs index ea175a309..d4aec19ae 100644 --- a/crates/uv-pep508/src/marker/tree.rs +++ b/crates/uv-pep508/src/marker/tree.rs @@ -33,12 +33,9 @@ pub enum MarkerWarningKind { /// Doing an operation other than `==` and `!=` on a quoted string with `extra`, such as /// `extra > "perf"` or `extra == os_name` ExtraInvalidComparison, - /// Doing an operation other than `in` and `not in` on a quoted string with `extra`, such as - /// `extras > "perf"` or `extras == os_name` - ExtrasInvalidComparison, - /// Doing an operation other than `in` and `not in` on a quoted string with `dependency_groups`, - /// such as `dependency_groups > "perf"` or `dependency_groups == os_name` - DependencyGroupsInvalidComparison, + /// Doing an operation other than `in` and `not in` on a list marker, such as + /// `extras > "perf"` or `dependency_groups == os_name` + ListInvalidComparison, /// Comparing a string valued marker and a string lexicographically, such as `"3.9" > "3.10"` LexicographicComparison, /// Comparing two markers, such as `os_name != sys_implementation` @@ -128,9 +125,15 @@ impl Display for MarkerValueString { /// Those markers with exclusively `in` and `not in` operators. /// -/// Contains PEP 751 lockfile markers. +/// Contains the PEP 751 lockfile marker and the variant markers. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub enum MarkerValueList { + /// `variant_namespaces` + VariantNamespaces, + /// `variant_features` + VariantFeatures, + /// `variant_properties` + VariantProperties, /// `extras`. This one is special because it's a list, and user-provided Extras, /// `dependency_groups`. This one is special because it's a list, and user-provided @@ -142,6 +145,9 @@ impl Display for MarkerValueList { match self { Self::Extras => f.write_str("extras"), Self::DependencyGroups => f.write_str("dependency_groups"), + Self::VariantNamespaces => f.write_str("variant_namespaces"), + Self::VariantFeatures => f.write_str("variant_features"), + Self::VariantProperties => f.write_str("variant_properties"), } } } @@ -200,6 +206,9 @@ impl FromStr for MarkerValue { "sys.platform" => Self::MarkerEnvString(MarkerValueString::SysPlatformDeprecated), "extras" => Self::MarkerEnvList(MarkerValueList::Extras), "dependency_groups" => Self::MarkerEnvList(MarkerValueList::DependencyGroups), + "variant_namespaces" => Self::MarkerEnvList(MarkerValueList::VariantNamespaces), + "variant_features" => Self::MarkerEnvList(MarkerValueList::VariantFeatures), + "variant_properties" => Self::MarkerEnvList(MarkerValueList::VariantProperties), "extra" => Self::Extra, _ => return Err(format!("Invalid key: {s}")), }; @@ -531,7 +540,7 @@ pub enum MarkerExpression { operator: MarkerOperator, value: ArcStr, }, - /// `'...' in `, a PEP 751 expression. + /// `'...' in `, either PEP 751 or a variant expression. List { pair: CanonicalMarkerListPair, operator: ContainerOperator, @@ -552,7 +561,8 @@ pub(crate) enum MarkerExpressionKind { VersionIn(MarkerValueVersion), /// A string marker comparison, e.g. `sys_platform == '...'`. String(MarkerValueString), - /// A list `in` or `not in` expression, e.g. `'...' in dependency_groups`. + /// A list `in` or `not in` expression, e.g. `'...' in dependency_groups` or + /// `'gpu :: cuda :: cu128' in variant_properties`. List(MarkerValueList), /// An extra expression, e.g. `extra == '...'`. Extra, @@ -996,10 +1006,16 @@ impl MarkerTree { } /// Does this marker apply in the given environment? - pub fn evaluate(self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool { + pub fn evaluate( + self, + env: &MarkerEnvironment, + variants: Option<&[(String, String, String)]>, + extras: &[ExtraName], + ) -> bool { self.evaluate_reporter_impl( env, ExtrasEnvironment::from_extras(extras), + variants, &mut TracingReporter, ) } @@ -1010,12 +1026,14 @@ impl MarkerTree { pub fn evaluate_pep751( self, env: &MarkerEnvironment, + variants: Option<&[(String, String, String)]>, extras: &[ExtraName], groups: &[GroupName], ) -> bool { self.evaluate_reporter_impl( env, ExtrasEnvironment::from_pep751(extras, groups), + variants, &mut TracingReporter, ) } @@ -1030,6 +1048,7 @@ impl MarkerTree { pub fn evaluate_optional_environment( self, env: Option<&MarkerEnvironment>, + variants: Option<&[(String, String, String)]>, extras: &[ExtraName], ) -> bool { match env { @@ -1037,6 +1056,7 @@ impl MarkerTree { Some(env) => self.evaluate_reporter_impl( env, ExtrasEnvironment::from_extras(extras), + variants, &mut TracingReporter, ), } @@ -1048,15 +1068,22 @@ impl MarkerTree { self, env: &MarkerEnvironment, extras: &[ExtraName], + variants: Option<&[(String, String, String)]>, reporter: &mut impl Reporter, ) -> bool { - self.evaluate_reporter_impl(env, ExtrasEnvironment::from_extras(extras), reporter) + self.evaluate_reporter_impl( + env, + ExtrasEnvironment::from_extras(extras), + variants, + reporter, + ) } fn evaluate_reporter_impl( self, env: &MarkerEnvironment, extras: ExtrasEnvironment, + variants: Option<&[(String, String, String)]>, reporter: &mut impl Reporter, ) -> bool { match self.kind() { @@ -1065,7 +1092,7 @@ impl MarkerTree { MarkerTreeKind::Version(marker) => { for (range, tree) in marker.edges() { if range.contains(env.get_version(marker.key())) { - return tree.evaluate_reporter_impl(env, extras, reporter); + return tree.evaluate_reporter_impl(env, extras, variants, reporter); } } } @@ -1092,27 +1119,56 @@ impl MarkerTree { } if range.contains(l_string) { - return tree.evaluate_reporter_impl(env, extras, reporter); + return tree.evaluate_reporter_impl(env, extras, variants, reporter); } } } MarkerTreeKind::In(marker) => { return marker .edge(marker.value().contains(env.get_string(marker.key()))) - .evaluate_reporter_impl(env, extras, reporter); + .evaluate_reporter_impl(env, extras, variants, reporter); } MarkerTreeKind::Contains(marker) => { return marker .edge(env.get_string(marker.key()).contains(marker.value())) - .evaluate_reporter_impl(env, extras, reporter); - } - MarkerTreeKind::Extra(marker) => { - return marker - .edge(extras.extra().contains(marker.name().extra())) - .evaluate_reporter_impl(env, extras, reporter); + .evaluate_reporter_impl(env, extras, variants, reporter); } MarkerTreeKind::List(marker) => { let edge = match marker.pair() { + CanonicalMarkerListPair::VariantNamespaces(marker_namespace) => { + let Some(variants) = variants else { + // If we're not limiting to specific variants, we're solving universally. + return true; + }; + + variants + .iter() + .any(|(namespace, _feature, _property)| namespace == marker_namespace) + } + CanonicalMarkerListPair::VariantFeatures(marker_namespace, marker_feature) => { + let Some(variants) = variants else { + return true; + }; + + variants.iter().any(|(namespace, feature, _property)| { + namespace == marker_namespace && feature == marker_feature + }) + } + CanonicalMarkerListPair::VariantProperties( + marker_namespace, + marker_feature, + marker_property, + ) => { + let Some(variants) = variants else { + return true; + }; + + variants.iter().any(|(namespace, feature, property)| { + namespace == marker_namespace + && feature == marker_feature + && property == marker_property + }) + } CanonicalMarkerListPair::Extras(extra) => extras.extras().contains(extra), CanonicalMarkerListPair::DependencyGroup(dependency_group) => { extras.dependency_groups().contains(dependency_group) @@ -1123,7 +1179,12 @@ impl MarkerTree { return marker .edge(edge) - .evaluate_reporter_impl(env, extras, reporter); + .evaluate_reporter_impl(env, extras, variants, reporter); + } + MarkerTreeKind::Extra(marker) => { + return marker + .edge(extras.extra().contains(marker.name().extra())) + .evaluate_reporter_impl(env, extras, variants, reporter); } } @@ -2150,12 +2211,12 @@ mod test { let marker3 = MarkerTree::from_str( "python_version == \"2.7\" and (sys_platform == \"win32\" or sys_platform == \"linux\")", ).unwrap(); - assert!(marker1.evaluate(&env27, &[])); - assert!(!marker1.evaluate(&env37, &[])); - assert!(marker2.evaluate(&env27, &[])); - assert!(marker2.evaluate(&env37, &[])); - assert!(marker3.evaluate(&env27, &[])); - assert!(!marker3.evaluate(&env37, &[])); + assert!(marker1.evaluate(&env27, None, &[])); + assert!(!marker1.evaluate(&env37, None, &[])); + assert!(marker2.evaluate(&env27, None, &[])); + assert!(marker2.evaluate(&env37, None, &[])); + assert!(marker3.evaluate(&env27, None, &[])); + assert!(!marker3.evaluate(&env37, None, &[])); } #[test] @@ -2177,48 +2238,48 @@ mod test { let env37 = env37(); let marker = MarkerTree::from_str("python_version in \"2.7 3.2 3.3\"").unwrap(); - assert!(marker.evaluate(&env27, &[])); - assert!(!marker.evaluate(&env37, &[])); + assert!(marker.evaluate(&env27, None, &[])); + assert!(!marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("python_version in \"2.7 3.7\"").unwrap(); - assert!(marker.evaluate(&env27, &[])); - assert!(marker.evaluate(&env37, &[])); + assert!(marker.evaluate(&env27, None, &[])); + assert!(marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("python_version in \"2.4 3.8 4.0\"").unwrap(); - assert!(!marker.evaluate(&env27, &[])); - assert!(!marker.evaluate(&env37, &[])); + assert!(!marker.evaluate(&env27, None, &[])); + assert!(!marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("python_version not in \"2.7 3.2 3.3\"").unwrap(); - assert!(!marker.evaluate(&env27, &[])); - assert!(marker.evaluate(&env37, &[])); + assert!(!marker.evaluate(&env27, None, &[])); + assert!(marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("python_version not in \"2.7 3.7\"").unwrap(); - assert!(!marker.evaluate(&env27, &[])); - assert!(!marker.evaluate(&env37, &[])); + assert!(!marker.evaluate(&env27, None, &[])); + assert!(!marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("python_version not in \"2.4 3.8 4.0\"").unwrap(); - assert!(marker.evaluate(&env27, &[])); - assert!(marker.evaluate(&env37, &[])); + assert!(marker.evaluate(&env27, None, &[])); + assert!(marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("python_full_version in \"2.7\"").unwrap(); - assert!(marker.evaluate(&env27, &[])); - assert!(!marker.evaluate(&env37, &[])); + assert!(marker.evaluate(&env27, None, &[])); + assert!(!marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("implementation_version in \"2.7 3.2 3.3\"").unwrap(); - assert!(marker.evaluate(&env27, &[])); - assert!(!marker.evaluate(&env37, &[])); + assert!(marker.evaluate(&env27, None, &[])); + assert!(!marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("implementation_version in \"2.7 3.7\"").unwrap(); - assert!(marker.evaluate(&env27, &[])); - assert!(marker.evaluate(&env37, &[])); + assert!(marker.evaluate(&env27, None, &[])); + assert!(marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("implementation_version not in \"2.7 3.7\"").unwrap(); - assert!(!marker.evaluate(&env27, &[])); - assert!(!marker.evaluate(&env37, &[])); + assert!(!marker.evaluate(&env27, None, &[])); + assert!(!marker.evaluate(&env37, None, &[])); let marker = MarkerTree::from_str("implementation_version not in \"2.4 3.8 4.0\"").unwrap(); - assert!(marker.evaluate(&env27, &[])); - assert!(marker.evaluate(&env37, &[])); + assert!(marker.evaluate(&env27, None, &[])); + assert!(marker.evaluate(&env37, None, &[])); } #[test] @@ -2227,7 +2288,7 @@ mod test { fn warnings1() { let env37 = env37(); let compare_keys = MarkerTree::from_str("platform_version == sys_platform").unwrap(); - compare_keys.evaluate(&env37, &[]); + compare_keys.evaluate(&env37, None, &[]); logs_contain( "Comparing two markers with each other doesn't make any sense, will evaluate to false", ); @@ -2239,7 +2300,7 @@ mod test { fn warnings2() { let env37 = env37(); let non_pep440 = MarkerTree::from_str("python_version >= '3.9.'").unwrap(); - non_pep440.evaluate(&env37, &[]); + non_pep440.evaluate(&env37, None, &[]); logs_contain( "Expected PEP 440 version to compare with python_version, found `3.9.`, \ will evaluate to false: after parsing `3.9`, found `.`, which is \ @@ -2253,7 +2314,7 @@ mod test { fn warnings3() { let env37 = env37(); let string_string = MarkerTree::from_str("'b' >= 'a'").unwrap(); - string_string.evaluate(&env37, &[]); + string_string.evaluate(&env37, None, &[]); logs_contain( "Comparing two quoted strings with each other doesn't make sense: 'b' >= 'a', will evaluate to false", ); @@ -2265,7 +2326,7 @@ mod test { fn warnings4() { let env37 = env37(); let string_string = MarkerTree::from_str(r"os.name == 'posix' and platform.machine == 'x86_64' and platform.python_implementation == 'CPython' and 'Ubuntu' in platform.version and sys.platform == 'linux'").unwrap(); - string_string.evaluate(&env37, &[]); + string_string.evaluate(&env37, None, &[]); logs_assert(|lines: &[&str]| { let lines: Vec<_> = lines .iter() @@ -2297,18 +2358,18 @@ mod test { let env37 = env37(); let result = MarkerTree::from_str("python_version > '3.6'") .unwrap() - .evaluate(&env37, &[]); + .evaluate(&env37, None, &[]); assert!(result); let result = MarkerTree::from_str("'3.6' > python_version") .unwrap() - .evaluate(&env37, &[]); + .evaluate(&env37, None, &[]); assert!(!result); // Meaningless expressions are ignored, so this is always true. let result = MarkerTree::from_str("'3.*' == python_version") .unwrap() - .evaluate(&env37, &[]); + .evaluate(&env37, None, &[]); assert!(result); } @@ -2317,12 +2378,12 @@ mod test { let env37 = env37(); let result = MarkerTree::from_str("'nux' in sys_platform") .unwrap() - .evaluate(&env37, &[]); + .evaluate(&env37, None, &[]); assert!(result); let result = MarkerTree::from_str("sys_platform in 'nux'") .unwrap() - .evaluate(&env37, &[]); + .evaluate(&env37, None, &[]); assert!(!result); } @@ -2331,7 +2392,7 @@ mod test { let env37 = env37(); let result = MarkerTree::from_str("python_version == '3.7.*'") .unwrap() - .evaluate(&env37, &[]); + .evaluate(&env37, None, &[]); assert!(result); } @@ -2340,7 +2401,7 @@ mod test { let env37 = env37(); let result = MarkerTree::from_str("python_version ~= '3.7'") .unwrap() - .evaluate(&env37, &[]); + .evaluate(&env37, None, &[]); assert!(result); } @@ -3709,4 +3770,146 @@ mod test { assert!(!marker.evaluate_only_extras(std::slice::from_ref(&b))); assert!(marker.evaluate_only_extras(&[a.clone(), b.clone()])); } + + #[test] + fn marker_evaluation_variants() { + let env37 = env37(); + let gpu_namespaces = [("gpu".to_string(), "cuda".to_string(), "12.4".to_string())]; + let cpu_namespaces = [("cpu".to_string(), String::new(), String::new())]; + + // namespace variant markers + let marker1 = m("'gpu' in variant_namespaces"); + let marker2 = m("'gpu' not in variant_namespaces"); + + // If no variants are provided, we solve universally. + assert!(marker1.evaluate(&env37, None, &[])); + assert!(marker2.evaluate(&env37, None, &[])); + + assert!(marker1.evaluate(&env37, Some(&gpu_namespaces), &[])); + assert!(!marker1.evaluate(&env37, Some(&cpu_namespaces), &[])); + assert!(!marker2.evaluate(&env37, Some(&gpu_namespaces), &[])); + assert!(marker2.evaluate(&env37, Some(&cpu_namespaces), &[])); + + // property variant markers + let marker3 = m("'gpu :: cuda' in variant_features"); + let marker4 = m("'gpu :: rocm' in variant_features"); + + assert!(marker3.evaluate(&env37, None, &[])); + assert!(marker4.evaluate(&env37, None, &[])); + + assert!(marker3.evaluate(&env37, Some(&gpu_namespaces), &[])); + assert!(!marker3.evaluate(&env37, Some(&cpu_namespaces), &[])); + assert!(!marker4.evaluate(&env37, Some(&gpu_namespaces), &[])); + assert!(!marker4.evaluate(&env37, Some(&cpu_namespaces), &[])); + + // feature variant markers + let marker5 = m("'gpu :: cuda :: 12.4' in variant_properties"); + let marker6 = m("'gpu :: cuda :: 12.8' in variant_properties"); + + assert!(marker5.evaluate(&env37, None, &[])); + assert!(marker6.evaluate(&env37, None, &[])); + + assert!(marker5.evaluate(&env37, Some(&gpu_namespaces), &[])); + assert!(!marker5.evaluate(&env37, Some(&cpu_namespaces), &[])); + assert!(!marker6.evaluate(&env37, Some(&gpu_namespaces), &[])); + assert!(!marker6.evaluate(&env37, Some(&cpu_namespaces), &[])); + } + + #[test] + fn marker_evaluation_variants_combined() { + let env37 = env37(); + let namespaces = [ + ("gpu".to_string(), "cuda".to_string(), "12.4".to_string()), + ("gpu".to_string(), "cuda".to_string(), "12.6".to_string()), + ("cpu".to_string(), "x86_64".to_string(), "v1".to_string()), + ("cpu".to_string(), "x86_64".to_string(), "v2".to_string()), + ("cpu".to_string(), "x86_64".to_string(), "v3".to_string()), + ]; + + let marker1 = m("'gpu' in variant_namespaces \ + and 'cpu:: x86_64 :: v3' in variant_properties \ + and python_version >= '3.7' \ + and 'gpu :: rocm' not in variant_features"); + assert!(marker1.evaluate(&env37, None, &[])); + assert!(marker1.evaluate(&env37, Some(&namespaces), &[])); + + let marker2 = m("python_version >= '3.7' and 'gpu' not in variant_namespaces"); + assert!(marker2.evaluate(&env37, None, &[])); + assert!(!marker2.evaluate(&env37, Some(&namespaces), &[])); + } + + #[test] + fn variant_to_string() { + let assert_roundtrips = |marker| { + assert_eq!(m(marker).try_to_string().unwrap(), marker); + }; + assert_roundtrips("'gpu' in variant_namespaces"); + assert_roundtrips("'gpu' not in variant_namespaces"); + assert_roundtrips("'gpu :: cuda' in variant_properties"); + assert_roundtrips("'gpu :: cuda' not in variant_properties"); + assert_roundtrips("'gpu :: cuda :: 12.4' in variant_features"); + assert_roundtrips("'gpu :: cuda :: 12.8' not in variant_features"); + + // TODO(konsti): Implement normalization and test it. + } + + #[test] + fn variant_errors() { + let err = MarkerExpression::from_str(r"variant_namespaces in 'gpu'") + .unwrap_err() + .to_string(); + assert_snapshot!( + err, + @r" + The marker variant_namespaces must be on the right hand side of the expression + variant_namespaces in 'gpu' + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + " + ); + let err = MarkerExpression::from_str(r"'gpu :: cuda' == variant_properties") + .unwrap_err() + .to_string(); + assert_snapshot!( + err, + @r" + The operator == is not supported with the marker variant_properties, only the `in` and `not in` operators are supported + 'gpu :: cuda' == variant_properties + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + " + ); + // TODO(konsti): Test all cases systematically + } + + #[test] + fn variant_invalid() { + let env37 = env37(); + let namespaces = [("gpu".to_string(), "cuda".to_string(), "cuda126".to_string())]; + + let marker = m(r"'gpu :: cuda :: cuda126 :: gtx1080' in variant_properties"); + assert!(!marker.evaluate(&env37, Some(&namespaces), &[])); + } + + #[test] + fn torch_variant_marker() { + let env37 = env37(); + let cu126 = [("nvidia".to_string(), "ctk".to_string(), "12.6".to_string())]; + let cu126_2 = [ + ("nvidia".to_string(), "ctk".to_string(), "12.6".to_string()), + ( + "nvidia".to_string(), + "cuda_version".to_string(), + ">=12.6,<13".to_string(), + ), + ]; + let cu128 = [("nvidia".to_string(), "ctk".to_string(), "12.8".to_string())]; + + let marker = m( + " platform_machine == 'x86_64' and sys_platform == 'linux' and 'nvidia :: ctk :: 12.6' in variant_properties", + ); + + assert!(marker.evaluate(&env37, None, &[])); + assert!(marker.evaluate(&env37, Some(&cu126), &[])); + assert!(marker.evaluate(&env37, Some(&cu126_2), &[])); + assert!(!marker.evaluate(&env37, Some(&cu128), &[])); + } } diff --git a/crates/uv-pep508/src/unnamed.rs b/crates/uv-pep508/src/unnamed.rs index d5c1820bb..2bf74058a 100644 --- a/crates/uv-pep508/src/unnamed.rs +++ b/crates/uv-pep508/src/unnamed.rs @@ -82,17 +82,24 @@ pub struct UnnamedRequirement { impl UnnamedRequirement { /// Returns whether the markers apply for the given environment - pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool { - self.evaluate_optional_environment(Some(env), extras) + pub fn evaluate_markers( + &self, + env: &MarkerEnvironment, + variants: Option<&[(String, String, String)]>, + extras: &[ExtraName], + ) -> bool { + self.evaluate_optional_environment(Some(env), variants, extras) } /// Returns whether the markers apply for the given environment pub fn evaluate_optional_environment( &self, env: Option<&MarkerEnvironment>, + variants: Option<&[(String, String, String)]>, extras: &[ExtraName], ) -> bool { - self.marker.evaluate_optional_environment(env, extras) + self.marker + .evaluate_optional_environment(env, variants, extras) } /// Set the source file containing the requirement.