From 3af137639d25e825b15ce80bb5e34f39dd6e64a6 Mon Sep 17 00:00:00 2001 From: RITANKAR SAHA Date: Fri, 6 Mar 2026 02:31:51 +0530 Subject: [PATCH] fix(conda_types): parse features and license_family in MatchSpec fix(conda_types): fixed ci fixed ci --- .../rattler_conda_types/src/match_spec/mod.rs | 54 +++++++++++++++++++ .../src/match_spec/parse.rs | 51 +++++++++++++++++- ...ec__parse__tests__matchspec_to_string.snap | 3 +- py-rattler/Cargo.lock | 1 + 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/crates/rattler_conda_types/src/match_spec/mod.rs b/crates/rattler_conda_types/src/match_spec/mod.rs index ac0a02e42c..845db54d28 100644 --- a/crates/rattler_conda_types/src/match_spec/mod.rs +++ b/crates/rattler_conda_types/src/match_spec/mod.rs @@ -166,6 +166,10 @@ pub struct MatchSpec { pub url: Option, /// The license of the package pub license: Option, + /// The license family of the package (e.g. `MIT`, `GPL`, `BSD`) + pub license_family: Option, + /// The features of the package (deprecated conda field, e.g. `blas`) + pub features: Option, /// The condition under which this match spec applies. pub condition: Option, /// The track features of the package @@ -229,6 +233,14 @@ impl Display for MatchSpec { keys.push(format!("license=\"{license}\"")); } + if let Some(license_family) = &self.license_family { + keys.push(format!("license_family=\"{license_family}\"")); + } + + if let Some(features) = &self.features { + keys.push(format!("features=\"{features}\"")); + } + if let Some(track_features) = &self.track_features { keys.push(format!( "track_features=\"{}\"", @@ -267,6 +279,8 @@ impl MatchSpec { sha256: self.sha256, url: self.url, license: self.license, + license_family: self.license_family, + features: self.features, condition: self.condition, track_features: self.track_features, }, @@ -329,6 +343,10 @@ pub struct NamelessMatchSpec { pub url: Option, /// The license of the package pub license: Option, + /// The license family of the package (e.g. `MIT`, `GPL`, `BSD`) + pub license_family: Option, + /// The features of the package + pub features: Option, /// The condition under which this match spec applies. pub condition: Option, /// The track features of the package @@ -356,6 +374,14 @@ impl Display for NamelessMatchSpec { keys.push(format!("sha256=\"{sha256:x}\"")); } + if let Some(license_family) = &self.license_family { + keys.push(format!("license_family=\"{license_family}\"")); + } + + if let Some(features) = &self.features { + keys.push(format!("features=\"{features}\"")); + } + if let Some(condition) = &self.condition { let condition_str = condition.to_string(); keys.push(format!("when=\"{}\"", escape_bracket_value(&condition_str))); @@ -384,6 +410,8 @@ impl From for NamelessMatchSpec { sha256: spec.sha256, url: spec.url, license: spec.license, + license_family: spec.license_family, + features: spec.features, condition: spec.condition, track_features: spec.track_features, } @@ -407,6 +435,8 @@ impl MatchSpec { sha256: spec.sha256, url: spec.url, license: spec.license, + license_family: spec.license_family, + features: spec.features, condition: spec.condition, track_features: spec.track_features, } @@ -482,6 +512,18 @@ impl Matches for NamelessMatchSpec { } } + if let Some(license_family) = self.license_family.as_ref() { + if Some(license_family) != other.license_family.as_ref() { + return false; + } + } + + if let Some(features) = self.features.as_ref() { + if Some(features) != other.features.as_ref() { + return false; + } + } + if let Some(track_features) = self.track_features.as_ref() { for feature in track_features { if !other.track_features.contains(feature) { @@ -537,6 +579,18 @@ impl Matches for MatchSpec { } } + if let Some(license_family) = self.license_family.as_ref() { + if Some(license_family) != other.license_family.as_ref() { + return false; + } + } + + if let Some(features) = self.features.as_ref() { + if Some(features) != other.features.as_ref() { + return false; + } + } + if let Some(track_features) = self.track_features.as_ref() { for feature in track_features { if !other.track_features.contains(feature) { diff --git a/crates/rattler_conda_types/src/match_spec/parse.rs b/crates/rattler_conda_types/src/match_spec/parse.rs index d9474b562f..ce57e26be9 100644 --- a/crates/rattler_conda_types/src/match_spec/parse.rs +++ b/crates/rattler_conda_types/src/match_spec/parse.rs @@ -520,8 +520,8 @@ fn parse_bracket_vec_into_components( return Err(ParseMatchSpecError::InvalidBracketKey("when".to_string())); } } - // TODO: Still need to add `features` and `license_family` - // to the match spec. + "license_family" => match_spec.license_family = Some(value.to_string()), + "features" => match_spec.features = Some(value.to_string()), _ => Err(ParseMatchSpecError::InvalidBracketKey(key.to_owned()))?, } } @@ -1610,6 +1610,51 @@ mod tests { assert_eq!(spec.license, Some("MIT".into())); } + #[test] + fn test_parsing_license_family() { + let spec = MatchSpec::from_str("python[license_family=MIT]", Strict).unwrap(); + assert_eq!(spec.license_family, Some("MIT".into())); + + // Roundtrip: Display -> parse must produce the same spec. + let reparsed = MatchSpec::from_str(&spec.to_string(), Strict).unwrap(); + assert_eq!(reparsed.license_family, Some("MIT".into())); + } + + #[test] + fn test_parsing_features() { + let spec = MatchSpec::from_str("numpy[features=blas]", Strict).unwrap(); + assert_eq!(spec.features, Some("blas".into())); + + // Roundtrip: Display -> parse must produce the same spec. + let reparsed = MatchSpec::from_str(&spec.to_string(), Strict).unwrap(); + assert_eq!(reparsed.features, Some("blas".into())); + } + + #[test] + fn test_features_and_license_family_matching() { + use crate::{match_spec::Matches, PackageName, PackageRecord, Version}; + + let mut record = PackageRecord::new( + PackageName::from_str("numpy").unwrap(), + Version::from_str("1.24.0").unwrap(), + "py310h1234_0".to_string(), + ); + record.features = Some("blas".to_string()); + record.license_family = Some("MIT".to_string()); + + // Both fields match. + let spec = MatchSpec::from_str("numpy[license_family=MIT, features=blas]", Strict).unwrap(); + assert!(spec.matches(&record)); + + // license_family mismatch. + let spec = MatchSpec::from_str("numpy[license_family=GPL]", Strict).unwrap(); + assert!(!spec.matches(&record)); + + // features mismatch. + let spec = MatchSpec::from_str("numpy[features=openblas]", Strict).unwrap(); + assert!(!spec.matches(&record)); + } + #[test] fn test_parsing_track_features() { let cases = vec![ @@ -1714,6 +1759,8 @@ mod tests { .unwrap(), ), license: Some("MIT".into()), + license_family: Some("MIT".into()), + features: Some("blas".into()), condition: None, track_features: None, }); diff --git a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__matchspec_to_string.snap b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__matchspec_to_string.snap index f48fb5b0ce..ad85693653 100644 --- a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__matchspec_to_string.snap +++ b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__matchspec_to_string.snap @@ -1,8 +1,9 @@ --- source: crates/rattler_conda_types/src/match_spec/parse.rs +assertion_line: 1770 expression: vec_strings --- [ "foo 1.0.*[build_number=\">6\"]", - "conda-forge/linux-64:foospace:foo 1.0.* py27_0*[md5=\"8b1a9953c4611296a827abf8c47804d7\", sha256=\"315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3\", build_number=\">=6\", fn=\"foo-1.0-py27_0.tar.bz2\", url=\"https://conda.anaconda.org/conda-forge/linux-64/foo-1.0-py27_0.tar.bz2\", license=\"MIT\"]", + "conda-forge/linux-64:foospace:foo 1.0.* py27_0*[md5=\"8b1a9953c4611296a827abf8c47804d7\", sha256=\"315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3\", build_number=\">=6\", fn=\"foo-1.0-py27_0.tar.bz2\", url=\"https://conda.anaconda.org/conda-forge/linux-64/foo-1.0-py27_0.tar.bz2\", license=\"MIT\", license_family=\"MIT\", features=\"blas\"]", ] diff --git a/py-rattler/Cargo.lock b/py-rattler/Cargo.lock index 3dc5df6df4..b048c6b99b 100644 --- a/py-rattler/Cargo.lock +++ b/py-rattler/Cargo.lock @@ -4244,6 +4244,7 @@ dependencies = [ "serde_json", "tempfile", "thiserror 2.0.18", + "tokio", "tracing", "url", ]