diff --git a/crates/rattler/src/install/installer/mod.rs b/crates/rattler/src/install/installer/mod.rs index f86f2a9b3..600bdc499 100644 --- a/crates/rattler/src/install/installer/mod.rs +++ b/crates/rattler/src/install/installer/mod.rs @@ -35,7 +35,7 @@ use itertools::Itertools; use rattler_cache::package_cache::{CacheLock, CacheReporter}; use rattler_conda_types::{ prefix_record::{Link, LinkType}, - MatchSpec, PackageName, Platform, PrefixRecord, RepoDataRecord, + MatchSpec, PackageName, PackageNameMatcher, Platform, PrefixRecord, RepoDataRecord, }; use rattler_networking::retry_policies::default_retry_policy; use rattler_networking::LazyClient; @@ -830,13 +830,13 @@ fn update_requested_specs_in_json( /// - The value is a vector of string representations of all matching /// `MatchSpecs` /// -/// `MatchSpecs` without names are skipped. -/// For multiple `MatchSpecs` with the same package name, all are collected. +/// Only `MatchSpec`s that have a `PackageNameMatcher::Exact` are included. +/// For multiple `MatchSpec`s with the same package name, all are collected. fn create_spec_mapping(specs: &[MatchSpec]) -> std::collections::HashMap> { let mut mapping = std::collections::HashMap::new(); for spec in specs { - if let Some(name) = &spec.name { + if let Some(PackageNameMatcher::Exact(name)) = &spec.name { mapping .entry(name.clone()) .or_insert_with(Vec::new) diff --git a/crates/rattler_conda_types/src/lib.rs b/crates/rattler_conda_types/src/lib.rs index 3789fb599..cdb26bf3c 100644 --- a/crates/rattler_conda_types/src/lib.rs +++ b/crates/rattler_conda_types/src/lib.rs @@ -7,7 +7,7 @@ mod build_spec; mod channel; mod channel_data; mod explicit_environment_spec; -mod match_spec; +pub mod match_spec; pub mod menuinst; mod no_arch_type; mod parse_mode; @@ -41,6 +41,7 @@ pub use explicit_environment_spec::{ ParseExplicitEnvironmentSpecError, ParsePackageArchiveHashError, }; pub use generic_virtual_package::GenericVirtualPackage; +pub use match_spec::package_name_matcher::{PackageNameMatcher, PackageNameMatcherParseError}; pub use match_spec::{ matcher::{StringMatcher, StringMatcherParseError}, parse::ParseMatchSpecError, diff --git a/crates/rattler_conda_types/src/match_spec/mod.rs b/crates/rattler_conda_types/src/match_spec/mod.rs index debea457d..198818688 100644 --- a/crates/rattler_conda_types/src/match_spec/mod.rs +++ b/crates/rattler_conda_types/src/match_spec/mod.rs @@ -1,3 +1,4 @@ +//! Query language for conda packages. use crate::package::ArchiveIdentifier; use crate::{ build_spec::BuildNumberSpec, GenericVirtualPackage, PackageName, PackageRecord, RepoDataRecord, @@ -17,10 +18,15 @@ use url::Url; use crate::Channel; use crate::ChannelConfig; +/// Match a given string either by exact match, glob or regex pub mod matcher; +/// Match package names either by exact match, glob or regex +pub mod package_name_matcher; +/// Parse a match spec from a string pub mod parse; use matcher::StringMatcher; +use package_name_matcher::PackageNameMatcher; /// A [`MatchSpec`] is, fundamentally, a query language for conda packages. Any of the fields that /// comprise a [`crate::PackageRecord`] can be used to compose a [`MatchSpec`]. @@ -76,41 +82,41 @@ use matcher::StringMatcher; /// # Examples: /// /// ```rust -/// use rattler_conda_types::{MatchSpec, VersionSpec, StringMatcher, PackageName, Channel, ChannelConfig, ParseStrictness::*}; +/// use rattler_conda_types::{MatchSpec, VersionSpec, StringMatcher, PackageNameMatcher, PackageName, Channel, ChannelConfig, ParseStrictness::*}; /// use std::str::FromStr; /// use std::sync::Arc; /// /// let channel_config = ChannelConfig::default_with_root_dir(std::env::current_dir().unwrap()); /// let spec = MatchSpec::from_str("foo 1.0.* py27_0", Strict).unwrap(); -/// assert_eq!(spec.name, Some(PackageName::new_unchecked("foo"))); +/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo")))); /// assert_eq!(spec.version, Some(VersionSpec::from_str("1.0.*", Strict).unwrap())); /// assert_eq!(spec.build, Some(StringMatcher::from_str("py27_0").unwrap())); /// /// let spec = MatchSpec::from_str("foo ==1.0 py27_0", Strict).unwrap(); -/// assert_eq!(spec.name, Some(PackageName::new_unchecked("foo"))); +/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo")))); /// assert_eq!(spec.version, Some(VersionSpec::from_str("==1.0", Strict).unwrap())); /// assert_eq!(spec.build, Some(StringMatcher::from_str("py27_0").unwrap())); /// /// let spec = MatchSpec::from_str(r#"conda-forge::foo[version="1.0.*"]"#, Strict).unwrap(); -/// assert_eq!(spec.name, Some(PackageName::new_unchecked("foo"))); +/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo")))); /// assert_eq!(spec.version, Some(VersionSpec::from_str("1.0.*", Strict).unwrap())); /// assert_eq!(spec.channel, Some(Channel::from_str("conda-forge", &channel_config).map(|channel| Arc::new(channel)).unwrap())); /// /// let spec = MatchSpec::from_str(r#"conda-forge::foo >=1.0[subdir="linux-64"]"#, Strict).unwrap(); -/// assert_eq!(spec.name, Some(PackageName::new_unchecked("foo"))); +/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo")))); /// assert_eq!(spec.version, Some(VersionSpec::from_str(">=1.0", Strict).unwrap())); /// assert_eq!(spec.channel, Some(Channel::from_str("conda-forge", &channel_config).map(|channel| Arc::new(channel)).unwrap())); /// assert_eq!(spec.subdir, Some("linux-64".to_string())); /// assert_eq!(spec, MatchSpec::from_str("conda-forge/linux-64::foo >=1.0", Strict).unwrap()); /// /// let spec = MatchSpec::from_str("*/linux-64::foo >=1.0", Strict).unwrap(); -/// assert_eq!(spec.name, Some(PackageName::new_unchecked("foo"))); +/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo")))); /// assert_eq!(spec.version, Some(VersionSpec::from_str(">=1.0", Strict).unwrap())); /// assert_eq!(spec.channel, Some(Channel::from_str("*", &channel_config).map(|channel| Arc::new(channel)).unwrap())); /// assert_eq!(spec.subdir, Some("linux-64".to_string())); /// /// let spec = MatchSpec::from_str(r#"foo[build="py2*"]"#, Strict).unwrap(); -/// assert_eq!(spec.name, Some(PackageName::new_unchecked("foo"))); +/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo")))); /// assert_eq!(spec.build, Some(StringMatcher::from_str("py2*").unwrap())); /// ``` /// @@ -130,7 +136,7 @@ use matcher::StringMatcher; #[derive(Debug, Default, Clone, Serialize, Eq, PartialEq, Hash)] pub struct MatchSpec { /// The name of the package - pub name: Option, + pub name: Option, /// The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`) pub version: Option, /// The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`) @@ -177,7 +183,7 @@ impl Display for MatchSpec { } match &self.name { - Some(name) => write!(f, "{}", name.as_normalized())?, + Some(name) => write!(f, "{name}")?, None => write!(f, "*")?, } @@ -229,7 +235,7 @@ impl Display for MatchSpec { impl MatchSpec { /// Decomposes this instance into a [`NamelessMatchSpec`] and a name. - pub fn into_nameless(self) -> (Option, NamelessMatchSpec) { + pub fn into_nameless(self) -> (Option, NamelessMatchSpec) { ( self.name, NamelessMatchSpec { @@ -252,10 +258,13 @@ impl MatchSpec { /// Returns whether the package is a virtual package. /// This is determined by the package name starting with `__`. /// Not having a package name is considered not virtual. + /// Matching both virtual and non-virtual packages is considered not virtual. pub fn is_virtual(&self) -> bool { - self.name - .as_ref() - .is_some_and(|name| name.as_normalized().starts_with("__")) + self.name.as_ref().is_some_and(|name| match name { + PackageNameMatcher::Exact(name) => name.as_normalized().starts_with("__"), + PackageNameMatcher::Glob(pattern) => pattern.as_str().starts_with("__"), + PackageNameMatcher::Regex(regex) => regex.as_str().starts_with(r"^__"), + }) } } @@ -263,7 +272,7 @@ impl MatchSpec { impl From for MatchSpec { fn from(value: PackageName) -> Self { Self { - name: Some(value), + name: Some(PackageNameMatcher::Exact(value)), ..Default::default() } } @@ -354,7 +363,7 @@ impl From for NamelessMatchSpec { impl MatchSpec { /// Constructs a [`MatchSpec`] from a [`NamelessMatchSpec`] and a name. - pub fn from_nameless(spec: NamelessMatchSpec, name: Option) -> Self { + pub fn from_nameless(spec: NamelessMatchSpec, name: Option) -> Self { Self { name, version: spec.version, @@ -450,7 +459,7 @@ impl Matches for MatchSpec { /// Match a [`MatchSpec`] against a [`PackageRecord`] fn matches(&self, other: &PackageRecord) -> bool { if let Some(name) = self.name.as_ref() { - if name != &other.name { + if !name.matches(&other.name) { return false; } } @@ -533,7 +542,7 @@ impl Matches for MatchSpec { /// Match a [`MatchSpec`] against a [`GenericVirtualPackage`] fn matches(&self, other: &GenericVirtualPackage) -> bool { if let Some(name) = self.name.as_ref() { - if name != &other.name { + if !name.matches(&other.name) { return false; } } @@ -590,8 +599,8 @@ impl TryFrom for MatchSpec { .ok_or(MatchSpecUrlError::InvalidFilename(filename.to_string()))?; spec.name = Some( - PackageName::from_str(&archive_identifier.name) - .map_err(|_err| MatchSpecUrlError::InvalidPackageName(archive_identifier.name))?, + PackageNameMatcher::from_str(&archive_identifier.name) + .map_err(|e| MatchSpecUrlError::InvalidPackageName(e.to_string()))?, ); Ok(spec) @@ -978,4 +987,40 @@ mod tests { MatchSpec::from_nameless(NamelessMatchSpec::from_str(">=12", Strict).unwrap(), None); assert!(!spec.is_virtual()); } + + #[test] + fn test_glob_in_name() { + let spec = MatchSpec::from_str("foo* >=12", Strict).unwrap(); + assert!(spec.matches(&PackageRecord::new( + PackageName::from_str("foo").unwrap(), + Version::from_str("13.0").unwrap(), + String::from(""), + ))); + assert!(!spec.matches(&PackageRecord::new( + PackageName::from_str("foo").unwrap(), + Version::from_str("11.0").unwrap(), + String::from(""), + ))); + assert!(spec.matches(&PackageRecord::new( + PackageName::from_str("foo-bar").unwrap(), + Version::from_str("12.0").unwrap(), + String::from(""), + ))); + + let spec = MatchSpec::from_str("foo* >=12[license=MIT]", Strict).unwrap(); + assert!(!spec.matches(&PackageRecord::new( + PackageName::from_str("foo-bar").unwrap(), + Version::from_str("12.0").unwrap(), + String::from(""), + ))); + assert!(spec.matches(&{ + let mut record = PackageRecord::new( + PackageName::from_str("foo-bar").unwrap(), + Version::from_str("12.0").unwrap(), + String::from(""), + ); + record.license = Some("MIT".into()); + record + })); + } } diff --git a/crates/rattler_conda_types/src/match_spec/package_name_matcher.rs b/crates/rattler_conda_types/src/match_spec/package_name_matcher.rs new file mode 100644 index 000000000..ad48ede3a --- /dev/null +++ b/crates/rattler_conda_types/src/match_spec/package_name_matcher.rs @@ -0,0 +1,193 @@ +use std::{ + borrow::Cow, + fmt::{Display, Formatter}, + hash::{Hash, Hasher}, + str::FromStr, +}; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{InvalidPackageNameError, PackageName}; + +/// Match a given string either by exact match, glob or regex +#[derive(Debug, Clone)] +pub enum PackageNameMatcher { + /// Match the string exactly + Exact(PackageName), + /// Match the string by glob. A glob uses a * to match any characters. + /// For example, `*` matches any string, `foo*` matches any string starting + /// with `foo`, `*bar` matches any string ending with `bar` and `foo*bar` + /// matches any string starting with `foo` and ending with `bar`. + Glob(glob::Pattern), + /// Match the string by regex. A regex starts with a `^`, ends with a `$` + /// and uses the regex syntax. For example, `^foo.*bar$` matches any + /// string starting with `foo` and ending with `bar`. Note that the regex + /// is anchored, so it must match the entire string. + Regex(regex::Regex), +} + +impl Hash for PackageNameMatcher { + fn hash(&self, state: &mut H) { + match self { + PackageNameMatcher::Exact(s) => s.hash(state), + PackageNameMatcher::Glob(pattern) => pattern.hash(state), + PackageNameMatcher::Regex(regex) => regex.as_str().hash(state), + } + } +} + +impl PartialEq for PackageNameMatcher { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (PackageNameMatcher::Exact(s1), PackageNameMatcher::Exact(s2)) => s1 == s2, + (PackageNameMatcher::Glob(s1), PackageNameMatcher::Glob(s2)) => { + s1.as_str() == s2.as_str() + } + (PackageNameMatcher::Regex(s1), PackageNameMatcher::Regex(s2)) => { + s1.as_str() == s2.as_str() + } + _ => false, + } + } +} + +impl PackageNameMatcher { + /// Match string against [`PackageNameMatcher`]. + pub fn matches(&self, other: &PackageName) -> bool { + match self { + PackageNameMatcher::Exact(s) => s == other, + PackageNameMatcher::Glob(glob) => glob.matches(other.as_normalized()), + PackageNameMatcher::Regex(regex) => regex.is_match(other.as_normalized()), + } + } +} + +impl From for Option { + fn from(value: PackageNameMatcher) -> Self { + match value { + PackageNameMatcher::Exact(s) => Some(s), + _ => None, + } + } +} + +/// Error when parsing [`PackageNameMatcher`] +#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)] +pub enum PackageNameMatcherParseError { + /// Could not parse the string as a glob + #[error("invalid glob: {glob}")] + Glob { + /// The invalid glob + glob: String, + }, + + /// Could not parse the string as a regex + #[error("invalid regex: {regex}")] + Regex { + /// The invalid regex + regex: String, + }, + + /// Could not parse the string as a package name + #[error("invalid package name {name}: {source}")] + PackageName { + /// The invalid package name + name: String, + + /// The source error + source: InvalidPackageNameError, + }, +} + +impl FromStr for PackageNameMatcher { + type Err = PackageNameMatcherParseError; + + fn from_str(s: &str) -> Result { + if s.starts_with('^') && s.ends_with('$') { + Ok(PackageNameMatcher::Regex(regex::Regex::new(s).map_err( + |_err| PackageNameMatcherParseError::Regex { + regex: s.to_string(), + }, + )?)) + } else if s.contains('*') { + Ok(PackageNameMatcher::Glob(glob::Pattern::new(s).map_err( + |_err| PackageNameMatcherParseError::Glob { + glob: s.to_string(), + }, + )?)) + } else { + Ok(PackageNameMatcher::Exact( + PackageName::from_str(s).map_err(|e| { + PackageNameMatcherParseError::PackageName { + name: s.to_string(), + source: e, + } + })?, + )) + } + } +} + +impl Display for PackageNameMatcher { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PackageNameMatcher::Exact(s) => write!(f, "{}", s.as_normalized()), + PackageNameMatcher::Glob(s) => write!(f, "{}", s.as_str()), + PackageNameMatcher::Regex(s) => write!(f, "{}", s.as_str()), + } + } +} + +impl Eq for PackageNameMatcher {} + +impl Serialize for PackageNameMatcher { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + PackageNameMatcher::Exact(s) => s.serialize(serializer), + PackageNameMatcher::Glob(s) => s.as_str().serialize(serializer), + PackageNameMatcher::Regex(s) => s.as_str().serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for PackageNameMatcher { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = Cow::<'de, str>::deserialize(deserializer)?; + PackageNameMatcher::from_str(&s).map_err(serde::de::Error::custom) + } +} + +/// Error when converting a [`PackageNameMatcher`] to a [`PackageName`] +#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)] +pub enum IntoPackageNameError { + /// The package name matcher is not an exact package name + #[error("not an exact package name")] + NotExact, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_package_name_matcher() { + assert_eq!( + PackageNameMatcher::Exact(PackageName::from_str("foo").unwrap()), + "foo".parse().unwrap() + ); + assert_eq!( + PackageNameMatcher::Glob(glob::Pattern::new("foo*bar").unwrap()), + "foo*bar".parse().unwrap() + ); + assert_eq!( + PackageNameMatcher::Regex(regex::Regex::new("^foo.*$").unwrap()), + "^foo.*$".parse().unwrap() + ); + } +} diff --git a/crates/rattler_conda_types/src/match_spec/parse.rs b/crates/rattler_conda_types/src/match_spec/parse.rs index 521850364..e8da7aca3 100644 --- a/crates/rattler_conda_types/src/match_spec/parse.rs +++ b/crates/rattler_conda_types/src/match_spec/parse.rs @@ -22,6 +22,7 @@ use super::{ }; use crate::{ build_spec::{BuildNumberSpec, ParseBuildNumberSpecError}, + match_spec::package_name_matcher::{PackageNameMatcher, PackageNameMatcherParseError}, package::ArchiveIdentifier, utils::{path::is_absolute_path, url::parse_scheme}, version_spec::{ @@ -29,9 +30,8 @@ use crate::{ version_tree::{recognize_constraint, recognize_version}, ParseVersionSpecError, }, - Channel, ChannelConfig, InvalidPackageNameError, NamelessMatchSpec, PackageName, - ParseChannelError, ParseStrictness, - ParseStrictness::{Lenient, Strict}, + Channel, ChannelConfig, NamelessMatchSpec, ParseChannelError, + ParseStrictness::{self, Lenient, Strict}, ParseVersionError, Platform, VersionSpec, }; @@ -96,9 +96,9 @@ pub enum ParseMatchSpecError { #[error("unable to parse hash digest from hex")] InvalidHashDigest, - /// The package name was invalid + /// The package name matcher was invalid #[error(transparent)] - InvalidPackageName(#[from] InvalidPackageNameError), + InvalidPackageNameMatcher(#[from] PackageNameMatcherParseError), /// Multiple values for a key in the matchspec #[error("found multiple values for: {0}")] @@ -362,7 +362,9 @@ pub fn parse_url_like(input: &str) -> Result, ParseMatchSpecError> { } /// Strip the package name from the input. -fn strip_package_name(input: &str) -> Result<(Option, &str), ParseMatchSpecError> { +fn strip_package_name( + input: &str, +) -> Result<(Option, &str), ParseMatchSpecError> { let (rest, package_name) = take_while1(|c: char| !c.is_whitespace() && !is_start_of_version_constraint(c))( input.trim(), @@ -375,14 +377,19 @@ fn strip_package_name(input: &str) -> Result<(Option, &str), ParseM return Err(ParseMatchSpecError::MissingPackageName); } + let rest = rest.trim(); + // Handle asterisk as a wildcard (no package name) if trimmed_package_name == "*" { - return Ok((None, rest.trim())); + return Ok((None, rest)); } Ok(( - Some(PackageName::from_str(trimmed_package_name)?), - rest.trim(), + Some( + PackageNameMatcher::from_str(trimmed_package_name) + .map_err(ParseMatchSpecError::InvalidPackageNameMatcher)?, + ), + rest, )) } @@ -628,7 +635,7 @@ fn matchspec_parser( if nameless_match_spec.url.is_none() { if let Some(url) = parse_url_like(&input)? { let archive = ArchiveIdentifier::try_from_url(&url); - let name = archive.and_then(|a| a.try_into().ok()); + let name = archive.and_then(|a| PackageNameMatcher::from_str(&a.name).ok()); // TODO: This should also work without a proper name from the url filename if name.is_none() { @@ -1352,7 +1359,7 @@ mod tests { .expect_err("Should try to parse as name not url"); assert_eq!( err.to_string(), - "'bla/bla' is not a valid package name. Package names can only contain 0-9, a-z, A-Z, -, _, or ." + "invalid package name bla/bla: 'bla/bla' is not a valid package name. Package names can only contain 0-9, a-z, A-Z, -, _, or ." ); } @@ -1368,7 +1375,7 @@ mod tests { fn test_issue_717() { assert_matches!( MatchSpec::from_str("ray[default,data] >=2.9.0,<3.0.0", Strict), - Err(ParseMatchSpecError::InvalidPackageName(_)) + Err(ParseMatchSpecError::InvalidPackageNameMatcher(_)) ); } diff --git a/crates/rattler_conda_types/src/package_name.rs b/crates/rattler_conda_types/src/package_name.rs index 1383ace27..998749033 100644 --- a/crates/rattler_conda_types/src/package_name.rs +++ b/crates/rattler_conda_types/src/package_name.rs @@ -48,7 +48,7 @@ impl PackageName { } /// An error that is returned when conversion from a string to a [`PackageName`] fails. -#[derive(Clone, Debug, Error, PartialEq)] +#[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum InvalidPackageNameError { /// The package name contains illegal characters #[error("'{0}' is not a valid package name. Package names can only contain 0-9, a-z, A-Z, -, _, or .")] diff --git a/crates/rattler_conda_types/src/repo_data/mod.rs b/crates/rattler_conda_types/src/repo_data/mod.rs index fc3217f2d..c9409503b 100644 --- a/crates/rattler_conda_types/src/repo_data/mod.rs +++ b/crates/rattler_conda_types/src/repo_data/mod.rs @@ -398,9 +398,10 @@ impl PackageRecord { for constraint in package.constrains.iter() { let constraint_spec = MatchSpec::from_str(constraint, ParseStrictness::Lenient) .map_err(ValidatePackageRecordsError::ParseMatchSpec)?; - let matching_package = records - .iter() - .find(|record| Some(record.as_ref().name.clone()) == constraint_spec.name); + let matching_package = records.iter().find(|record| match &constraint_spec.name { + Some(matcher) => matcher.matches(&record.as_ref().name), + None => false, + }); if matching_package.is_some_and(|p| !constraint_spec.matches(p.as_ref())) { return Err(Box::new( ValidatePackageRecordsError::PackageConstraintNotSatisfied { diff --git a/crates/rattler_lock/src/conda.rs b/crates/rattler_lock/src/conda.rs index c382834ff..07e3f8f0a 100644 --- a/crates/rattler_lock/src/conda.rs +++ b/crates/rattler_lock/src/conda.rs @@ -354,7 +354,7 @@ impl Matches for CondaPackageData { fn matches(&self, spec: &MatchSpec) -> bool { // Check if the name matches if let Some(name) = &spec.name { - if name != &self.record().name { + if !name.matches(&self.record().name) { return false; } } diff --git a/crates/rattler_lock/src/utils/serde/match_spec_map_or_vec.rs b/crates/rattler_lock/src/utils/serde/match_spec_map_or_vec.rs index 2ef6bd27a..d2b745dde 100644 --- a/crates/rattler_lock/src/utils/serde/match_spec_map_or_vec.rs +++ b/crates/rattler_lock/src/utils/serde/match_spec_map_or_vec.rs @@ -1,6 +1,8 @@ use fxhash::FxBuildHasher; use indexmap::IndexMap; -use rattler_conda_types::{MatchSpec, NamelessMatchSpec, PackageName}; +use rattler_conda_types::{ + match_spec::package_name_matcher::PackageNameMatcher, MatchSpec, NamelessMatchSpec, PackageName, +}; use serde::{Deserialize, Deserializer}; use serde_with::{serde_as, DeserializeAs, DisplayFromStr}; @@ -26,7 +28,10 @@ impl<'de> DeserializeAs<'de, Vec> for MatchSpecMapOrVec { MapOrVec::Vec(v) => v, MapOrVec::Map(m) => m .into_iter() - .map(|(name, spec)| MatchSpec::from_nameless(spec, Some(name)).to_string()) + .map(|(name, spec)| { + MatchSpec::from_nameless(spec, Some(PackageNameMatcher::Exact(name))) + .to_string() + }) .collect(), }) } diff --git a/crates/rattler_repodata_gateway/src/gateway/error.rs b/crates/rattler_repodata_gateway/src/gateway/error.rs index 6e7deaf29..6b9fedbac 100644 --- a/crates/rattler_repodata_gateway/src/gateway/error.rs +++ b/crates/rattler_repodata_gateway/src/gateway/error.rs @@ -46,8 +46,8 @@ pub enum GatewayError { #[source] super::direct_url_query::DirectUrlQueryError, ), - #[error("the match spec '{0}' does not specify a name")] - MatchSpecWithoutName(Box), + #[error("the match spec '{0}' does not specify an exact name")] + MatchSpecWithoutExactName(Box), #[error("the package from url '{0}', doesn't have the same name as the match spec filename intents '{1}'")] UrlRecordNameMismatch(String, String), diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index 6f8d007bc..4853f475e 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -577,7 +577,7 @@ mod test { .await .unwrap_err(); - assert_matches!(gateway_error, GatewayError::MatchSpecWithoutName(_)); + assert_matches!(gateway_error, GatewayError::MatchSpecWithoutExactName(_)); } #[rstest] diff --git a/crates/rattler_repodata_gateway/src/gateway/query.rs b/crates/rattler_repodata_gateway/src/gateway/query.rs index 8188c2ecc..a468ab176 100644 --- a/crates/rattler_repodata_gateway/src/gateway/query.rs +++ b/crates/rattler_repodata_gateway/src/gateway/query.rs @@ -6,7 +6,7 @@ use std::{ use futures::{select_biased, stream::FuturesUnordered, FutureExt, StreamExt}; use itertools::Itertools; -use rattler_conda_types::{Channel, MatchSpec, Matches, PackageName, Platform}; +use rattler_conda_types::{Channel, MatchSpec, Matches, PackageName, PackageNameMatcher, Platform}; use super::{subdir::Subdir, BarrierCell, GatewayError, GatewayInner, RepoData}; use crate::Reporter; @@ -112,15 +112,19 @@ impl RepoDataQuery { let mut seen = HashSet::new(); let mut pending_package_specs = HashMap::new(); let mut direct_url_specs = vec![]; + // TODO: allow glob/regex package names as well for spec in self.specs { if let Some(url) = spec.url.clone() { let name = spec .name .clone() - .ok_or(GatewayError::MatchSpecWithoutName(Box::new(spec.clone())))?; + .and_then(Option::::from) + .ok_or(GatewayError::MatchSpecWithoutExactName(Box::new( + spec.clone(), + )))?; seen.insert(name.clone()); - direct_url_specs.push((spec.clone(), url, name)); - } else if let Some(name) = &spec.name { + direct_url_specs.push((spec.clone(), url, name.clone())); + } else if let Some(PackageNameMatcher::Exact(name)) = &spec.name { seen.insert(name.clone()); let pending = pending_package_specs .entry(name.clone()) diff --git a/crates/rattler_repodata_gateway/src/sparse/mod.rs b/crates/rattler_repodata_gateway/src/sparse/mod.rs index 912b30ae0..e4b2ea044 100644 --- a/crates/rattler_repodata_gateway/src/sparse/mod.rs +++ b/crates/rattler_repodata_gateway/src/sparse/mod.rs @@ -260,8 +260,9 @@ impl SparseRepoData { let base_url = repo_data.info.as_ref().and_then(|i| i.base_url.as_deref()); for (package_name, specs) in &spec.into_iter().chunk_by(|spec| spec.borrow().name.clone()) { let grouped_specs = specs.into_iter().collect::>(); + // TODO: support glob/regex package names let mut parsed_records = parse_records( - package_name.as_ref(), + package_name.and_then(Option::::from).as_ref(), &repo_data.packages, &repo_data.conda_packages, variant_consolidation, diff --git a/crates/rattler_solve/benches/bench.rs b/crates/rattler_solve/benches/bench.rs index f905c6e0c..97feed235 100644 --- a/crates/rattler_solve/benches/bench.rs +++ b/crates/rattler_solve/benches/bench.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion, SamplingMode}; use rattler_conda_types::ParseStrictness::Strict; -use rattler_conda_types::{Channel, ChannelConfig, MatchSpec}; +use rattler_conda_types::{Channel, ChannelConfig, MatchSpec, PackageName}; use rattler_repodata_gateway::sparse::{PackageFormatSelection, SparseRepoData}; use rattler_solve::{SolverImpl, SolverTask}; use std::hint::black_box; @@ -55,7 +55,9 @@ fn bench_solve_environment(c: &mut Criterion, specs: Vec<&str>) { read_sparse_repodata(&json_file_noarch), ]; - let names = specs.iter().map(|s| s.name.clone().unwrap()); + let names = specs + .iter() + .map(|s| Option::::from(s.name.clone().unwrap()).unwrap()); let available_packages = SparseRepoData::load_records_recursive( &sparse_repo_data, names, diff --git a/crates/rattler_solve/benches/sorting_bench.rs b/crates/rattler_solve/benches/sorting_bench.rs index 772ee296d..659e377f4 100644 --- a/crates/rattler_solve/benches/sorting_bench.rs +++ b/crates/rattler_solve/benches/sorting_bench.rs @@ -2,7 +2,7 @@ use std::{hint::black_box, path::Path}; use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; use futures::FutureExt; -use rattler_conda_types::{Channel, MatchSpec}; +use rattler_conda_types::{Channel, MatchSpec, PackageName}; use rattler_repodata_gateway::sparse::{PackageFormatSelection, SparseRepoData}; use rattler_solve::{resolvo::CondaDependencyProvider, ChannelPriority}; use resolvo::SolverCache; @@ -10,7 +10,7 @@ use resolvo::SolverCache; fn bench_sort(c: &mut Criterion, sparse_repo_data: &SparseRepoData, spec: &str) { let match_spec = MatchSpec::from_str(spec, rattler_conda_types::ParseStrictness::Lenient).unwrap(); - let package_name = match_spec.name.clone().unwrap(); + let package_name = Option::::from(match_spec.name.clone().unwrap()).unwrap(); let repodata = SparseRepoData::load_records_recursive( [sparse_repo_data], diff --git a/crates/rattler_solve/src/libsolv_c/mod.rs b/crates/rattler_solve/src/libsolv_c/mod.rs index d9688477d..53377cd16 100644 --- a/crates/rattler_solve/src/libsolv_c/mod.rs +++ b/crates/rattler_solve/src/libsolv_c/mod.rs @@ -10,7 +10,10 @@ pub use input::cache_repodata; use input::{add_repodata_records, add_solv_file, add_virtual_packages}; pub use libc_byte_slice::LibcByteSlice; use output::get_required_packages; -use rattler_conda_types::{MatchSpec, NamelessMatchSpec, RepoDataRecord, SolverResult}; +use rattler_conda_types::{ + match_spec::package_name_matcher::PackageNameMatcher, MatchSpec, NamelessMatchSpec, + RepoDataRecord, SolverResult, +}; use wrapper::{ flags::SolverFlag, pool::{Pool, Verbosity}, @@ -254,7 +257,7 @@ impl super::SolverImpl for Solver { for virtual_package in task.virtual_packages { let id = pool.intern_matchspec(&MatchSpec::from_nameless( NamelessMatchSpec::default(), - Some(virtual_package.name), + Some(PackageNameMatcher::Exact(virtual_package.name)), )); goal.install(id, false); } diff --git a/crates/rattler_solve/src/resolvo/mod.rs b/crates/rattler_solve/src/resolvo/mod.rs index 787d22c60..fcce68f0f 100644 --- a/crates/rattler_solve/src/resolvo/mod.rs +++ b/crates/rattler_solve/src/resolvo/mod.rs @@ -13,7 +13,8 @@ use conda_sorting::SolvableSorter; use itertools::Itertools; use rattler_conda_types::{ package::ArchiveType, GenericVirtualPackage, MatchSpec, Matches, NamelessMatchSpec, - PackageName, ParseMatchSpecError, ParseStrictness, RepoDataRecord, SolverResult, + PackageName, PackageNameMatcher, ParseMatchSpecError, ParseStrictness, RepoDataRecord, + SolverResult, }; use resolvo::{ utils::{Pool, VersionSet}, @@ -297,7 +298,8 @@ impl<'a> CondaDependencyProvider<'a> { let direct_dependencies = match_specs .iter() .filter_map(|spec| spec.name.as_ref()) - .map(|name| pool.intern_package_name(name)) + .filter_map(|name| Option::::from(name.clone())) + .map(|name| pool.intern_package_name(&name)) .collect(); // TODO: Normalize these channel names to urls so we can compare them correctly. @@ -405,7 +407,8 @@ impl<'a> CondaDependencyProvider<'a> { if let Some(spec) = channel_specific_specs.iter().find(|&&spec| { spec.name .as_ref() - .expect("expecting a name") + .and_then(|name| Option::::from(name.clone())) + .expect("expecting an exact package name") .as_normalized() == record.package_record.name.as_normalized() }) { @@ -843,8 +846,9 @@ impl super::SolverImpl for Solver { .constraints .iter() .map(|spec| { - let (Some(name), spec) = spec.clone().into_nameless() else { - unimplemented!("matchspecs without a name are not supported"); + let (Some(PackageNameMatcher::Exact(name)), spec) = spec.clone().into_nameless() + else { + unimplemented!("only exact package names are supported"); }; let name_id = provider.pool.intern_package_name(&name); provider.pool.intern_version_set(name_id, spec.into()) @@ -916,8 +920,8 @@ fn version_sets_for_match_spec( pool: &Pool, NameType>, spec: MatchSpec, ) -> Vec { - let (Some(name), spec) = spec.into_nameless() else { - unimplemented!("matchspecs without a name are not supported"); + let (Some(PackageNameMatcher::Exact(name)), spec) = spec.into_nameless() else { + unimplemented!("only exact package names are supported"); }; // Add a dependency on each extra. diff --git a/crates/rattler_solve/tests/backends.rs b/crates/rattler_solve/tests/backends.rs index 0c526dd6b..650ea20ca 100644 --- a/crates/rattler_solve/tests/backends.rs +++ b/crates/rattler_solve/tests/backends.rs @@ -3,8 +3,8 @@ use std::{collections::BTreeMap, str::FromStr, time::Instant}; use chrono::{DateTime, Utc}; use once_cell::sync::Lazy; use rattler_conda_types::{ - Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, NoArchType, PackageRecord, - ParseStrictness, RepoData, RepoDataRecord, SolverResult, Version, + Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, NoArchType, PackageName, + PackageRecord, ParseStrictness, RepoData, RepoDataRecord, SolverResult, Version, }; use rattler_repodata_gateway::sparse::{PackageFormatSelection, SparseRepoData}; use rattler_solve::{ChannelPriority, SolveError, SolveStrategy, SolverImpl, SolverTask}; @@ -132,7 +132,11 @@ fn solve_real_world(specs: Vec<&str>) -> Vec { let sparse_repo_data = read_real_world_repo_data(); - let names = specs.iter().filter_map(|s| s.name.as_ref().cloned()); + let names = specs.iter().filter_map(|s| { + s.name + .as_ref() + .and_then(|n| Option::::from(n.clone())) + }); let available_packages = SparseRepoData::load_records_recursive( sparse_repo_data, names, @@ -1156,7 +1160,11 @@ fn compare_solve(task: CompareTask<'_>) { let sparse_repo_data = read_real_world_repo_data(); - let names = specs.iter().filter_map(|s| s.name.as_ref().cloned()); + let names = specs.iter().filter_map(|s| { + s.name + .as_ref() + .and_then(|n| Option::::from(n.clone())) + }); let available_packages = SparseRepoData::load_records_recursive( sparse_repo_data, names, @@ -1292,7 +1300,11 @@ fn solve_to_get_channel_of_spec( ) { let spec = MatchSpec::from_str(spec_str, ParseStrictness::Lenient).unwrap(); let specs = vec![spec.clone()]; - let names = specs.iter().filter_map(|s| s.name.as_ref().cloned()); + let names = specs.iter().filter_map(|s| { + s.name + .as_ref() + .and_then(|n| Option::::from(n.clone())) + }); let available_packages = SparseRepoData::load_records_recursive( repo_data, @@ -1311,7 +1323,10 @@ fn solve_to_get_channel_of_spec( let result: Vec = T::default().solve(task).unwrap().records; let record = result.iter().find(|record| { - record.package_record.name.as_normalized() == spec.name.as_ref().unwrap().as_normalized() + spec.name + .as_ref() + .unwrap() + .matches(&record.package_record.name) }); assert_eq!(record.unwrap().channel, Some(expected_channel.to_string())); } diff --git a/crates/rattler_solve/tests/snapshots/backends__resolvo__issue_717.snap b/crates/rattler_solve/tests/snapshots/backends__resolvo__issue_717.snap index c24dcaebb..4a33e63fc 100644 --- a/crates/rattler_solve/tests/snapshots/backends__resolvo__issue_717.snap +++ b/crates/rattler_solve/tests/snapshots/backends__resolvo__issue_717.snap @@ -4,4 +4,4 @@ expression: result.unwrap_err() --- Cannot solve the request because of: The following packages are incompatible └─ issue_717 * cannot be installed because there are no viable options: - └─ issue_717 2.1 is excluded because the constrains 'ray[default,data] >=2.9.0,<3.0.0' failed to parse: 'ray[default,data]' is not a valid package name. Package names can only contain 0-9, a-z, A-Z, -, _, or . + └─ issue_717 2.1 is excluded because the constrains 'ray[default,data] >=2.9.0,<3.0.0' failed to parse: invalid package name ray[default,data]: 'ray[default,data]' is not a valid package name. Package names can only contain 0-9, a-z, A-Z, -, _, or . diff --git a/crates/rattler_solve/tests/sorting.rs b/crates/rattler_solve/tests/sorting.rs index 54836ace0..bdea22d98 100644 --- a/crates/rattler_solve/tests/sorting.rs +++ b/crates/rattler_solve/tests/sorting.rs @@ -35,7 +35,7 @@ fn load_repodata(package_name: &PackageName) -> Vec> { fn create_sorting_snapshot(package_name: &str, strategy: SolveStrategy) -> String { let match_spec = MatchSpec::from_str(package_name, Lenient).unwrap(); - let package_name = match_spec.name.clone().unwrap(); + let package_name = Option::::from(match_spec.name.clone().unwrap()).unwrap(); // Load repodata let repodata = load_repodata(&package_name); diff --git a/py-rattler/Cargo.lock b/py-rattler/Cargo.lock index 8648195c5..dd4d7ae56 100644 --- a/py-rattler/Cargo.lock +++ b/py-rattler/Cargo.lock @@ -123,9 +123,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arbitrary" @@ -343,9 +343,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.7" +version = "1.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04b37ddf8d2e9744a0b9c19ce0b78efe4795339a90b66b7bae77987092cd2e69" +checksum = "37cf2b6af2a95a20e266782b4f76f1a5e12bf412a9db2de9c1e9123b9d8c0ad8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -373,9 +373,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.7" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a1290207254984cb7c05245111bc77958b92a3c9bb449598044b36341cce6" +checksum = "faf26925f4a5b59eb76722b63c2892b1d70d06fa053c72e4a100ec308c1d47bc" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -385,9 +385,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.11" +version = "1.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e1ed337dabcf765ad5f2fb426f13af22d576328aaf09eac8f70953530798ec0" +checksum = "bfa006bb32360ed90ac51203feafb9d02e3d21046e1fd3a450a404b90ea73e5d" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -410,9 +410,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.107.0" +version = "1.108.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9118b3454ba89b30df55931a1fa7605260fc648e070b5aab402c24b375b1f" +checksum = "200be4aed61e3c0669f7268bacb768f283f1c32a7014ce57225e1160be2f6ccb" dependencies = [ "aws-credential-types", "aws-runtime", @@ -444,9 +444,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.85.0" +version = "1.86.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f2c741e2e439f07b5d1b33155e246742353d82167c785a2ff547275b7e32483" +checksum = "4a0abbfab841446cce6e87af853a3ba2cc1bc9afcd3f3550dd556c43d434c86d" dependencies = [ "aws-credential-types", "aws-runtime", @@ -466,9 +466,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.87.0" +version = "1.88.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6428ae5686b18c0ee99f6f3c39d94ae3f8b42894cdc35c35d8fb2470e9db2d4c" +checksum = "9a68d675582afea0e94d38b6ca9c5aaae4ca14f1d36faa6edb19b42e687e70d7" dependencies = [ "aws-credential-types", "aws-runtime", @@ -488,9 +488,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.87.0" +version = "1.88.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5871bec9a79a3e8d928c7788d654f135dde0e71d2dd98089388bab36b37ef607" +checksum = "d30990923f4f675523c51eb1c0dec9b752fb267b36a61e83cbc219c9d86da715" dependencies = [ "aws-credential-types", "aws-runtime", @@ -511,9 +511,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.3.4" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084c34162187d39e3740cb635acd73c4e3a551a36146ad6fe8883c929c9f876c" +checksum = "bffc03068fbb9c8dd5ce1c6fb240678a5cffb86fb2b7b1985c999c4b83c8df68" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -539,9 +539,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" +checksum = "127fcfad33b7dfc531141fda7e1c402ac65f88aca5511a4d31e2e3d2cd01ce9c" dependencies = [ "futures-util", "pin-project-lite", @@ -550,9 +550,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.63.8" +version = "0.63.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d2df0314b8e307995a3b86d44565dfe9de41f876901a7d71886c756a25979f" +checksum = "165d8583d8d906e2fb5511d29201d447cc710864f075debcdd9c31c265412806" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -570,9 +570,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.11" +version = "0.60.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "182b03393e8c677347fb5705a04a9392695d47d20ef0a2f8cfe28c8e6b9b9778" +checksum = "9656b85088f8d9dc7ad40f9a6c7228e1e8447cdf4b046c87e152e0805dea02fa" dependencies = [ "aws-smithy-types", "bytes", @@ -581,9 +581,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.3" +version = "0.62.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c4dacf2d38996cf729f55e7a762b30918229917eca115de45dfa8dfb97796c9" +checksum = "3feafd437c763db26aa04e0cc7591185d0961e64c61885bece0fb9d50ceac671" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -602,9 +602,9 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734b4282fbb7372923ac339cc2222530f8180d9d4745e582de19a18cee409fd8" +checksum = "1053b5e587e6fa40ce5a79ea27957b04ba660baa02b28b7436f64850152234f1" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -626,27 +626,27 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.5" +version = "0.61.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaa31b350998e703e9826b2104dd6f63be0508666e1aba88137af060e8944047" +checksum = "cff418fc8ec5cadf8173b10125f05c2e7e1d46771406187b2c878557d4503390" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" +checksum = "2d1881b1ea6d313f9890710d65c158bdab6fb08c91ea825f74c1c8c357baf4cc" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.7" +version = "0.60.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +checksum = "d28a63441360c477465f80c7abac3b9c4d075ca638f982e605b7dc2a2c7156c9" dependencies = [ "aws-smithy-types", "urlencoding", @@ -654,9 +654,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa63ad37685ceb7762fa4d73d06f1d5493feb88e3f27259b9ed277f4c01b185" +checksum = "40ab99739082da5347660c556689256438defae3bcefd66c52b095905730e404" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -677,9 +677,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56" +checksum = "3683c5b152d2ad753607179ed71988e8cfd52964443b4f74fd8e552d0bbfeb46" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -694,9 +694,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" +checksum = "9f5b3a7486f6690ba25952cabf1e7d75e34d69eaff5081904a47bc79074d6457" dependencies = [ "base64-simd", "bytes", @@ -720,18 +720,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.10" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" +checksum = "e9c34127e8c624bc2999f3b657e749c1393bedc9cd97b92a804db8ced4d2e163" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.8" +version = "1.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" +checksum = "e2fd329bf0e901ff3f60425691410c69094dc2a1f34b331f37bfc4e9ac1565a1" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -1984,9 +1984,9 @@ dependencies = [ [[package]] name = "google-cloud-auth" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d8757f37377c6436b94c6efaf7f9c3c6e48a213bb0790f7a639a9ba0183764" +checksum = "c5a0f0ef58bc79d636e95db264939a6f3fd80951f77743f2b7ec55e22171150d" dependencies = [ "async-trait", "base64", @@ -2006,9 +2006,9 @@ dependencies = [ [[package]] name = "google-cloud-gax" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6b1718e2a6965a7fbb282f23645e7adb8e0a012848258bbd6f580307b684f" +checksum = "58bc95deae841e35758fa5caba317092f26940135c7184570feb691a1844db08" dependencies = [ "base64", "bytes", @@ -2026,9 +2026,9 @@ dependencies = [ [[package]] name = "google-cloud-rpc" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaa87d841e35c69efa61786e7e7462b4ebdb96df7be79775aeb698b4c0cc52e" +checksum = "e5b655e3540a78e18fd753ebd8f11e068210a3fa392892370f932ffcc8774346" dependencies = [ "bytes", "google-cloud-wkt", @@ -2039,9 +2039,9 @@ dependencies = [ [[package]] name = "google-cloud-wkt" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccfa292c272695eb966fe8b08c78cabb35e7775771a1c7f97b58f7dcb7bfb26" +checksum = "02931df6af9beda1c852bbbbe5f7b6ba6ae5e4cd49c029fa0ca2cecc787cd9b1" dependencies = [ "base64", "bytes", diff --git a/py-rattler/rattler/match_spec/match_spec.py b/py-rattler/rattler/match_spec/match_spec.py index fcb5c0bf2..b58855975 100644 --- a/py-rattler/rattler/match_spec/match_spec.py +++ b/py-rattler/rattler/match_spec/match_spec.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Optional from rattler.channel.channel import Channel -from rattler.package.package_name import PackageName +from rattler.package.package_name_matcher import PackageNameMatcher from rattler.rattler import PyMatchSpec if TYPE_CHECKING: @@ -104,11 +104,11 @@ def __init__(self, spec: str, strict: bool = False) -> None: ) @property - def name(self) -> Optional[PackageName]: + def name(self) -> Optional[PackageNameMatcher]: """ The name of the package. """ - return PackageName._from_py_package_name(self._match_spec.name) + return PackageNameMatcher._from_py_package_name_matcher(self._match_spec.name) @property def version(self) -> Optional[str]: @@ -203,7 +203,7 @@ def from_nameless(cls, spec: NamelessMatchSpec, name: str) -> MatchSpec: MatchSpec("foo ==3.4") >>> MatchSpec.from_nameless(spec, "$foo") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): - exceptions.InvalidPackageNameException + exceptions.PackageNameMatcherParseException >>> ``` """ diff --git a/py-rattler/rattler/package/package_name_matcher.py b/py-rattler/rattler/package/package_name_matcher.py new file mode 100644 index 000000000..bf7ec6a35 --- /dev/null +++ b/py-rattler/rattler/package/package_name_matcher.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import Union + +from rattler.package.package_name import PackageName +from rattler.rattler import PyPackageNameMatcher + + +class PackageNameMatcher: + """ + A class representing a package name matcher. + + Examples + -------- + ```python + >>> PackageNameMatcher("rattler") + PackageNameMatcher("rattler", exact) + >>> PackageNameMatcher("jupyter-*") + PackageNameMatcher("jupyter-*", glob) + >>> PackageNameMatcher("^jupyter-.*$") + PackageNameMatcher("^jupyter-.*$", regex) + >>> + ``` + """ + + _package_name_matcher: PyPackageNameMatcher + + def __init__(self, package_name_matcher: str): + self._package_name_matcher = PyPackageNameMatcher(package_name_matcher) + + def __repr__(self) -> str: + inner = self._package_name_matcher.display_inner() + return f"{type(self).__name__}({inner})" + + @classmethod + def _from_py_package_name_matcher(cls, py_package_name_matcher: PyPackageNameMatcher) -> PackageNameMatcher: + """Construct Rattler PackageNameMatcher from FFI PyPackageName object.""" + package_name_matcher = cls.__new__(cls) + package_name_matcher._package_name_matcher = py_package_name_matcher + return package_name_matcher + + def as_package_name(self) -> Union[PackageName, None]: + """ + Converts a PackageNameMatcher to a PackageName if it is an exact matcher. + + Examples + -------- + ```python + >>> PackageNameMatcher("rattler").as_package_name() + PackageName("rattler") + >>> PackageNameMatcher("jupyter-*").as_package_name() + >>> PackageNameMatcher("^jupyter-.*$").as_package_name() + >>> + ``` + """ + py_package_name = self._package_name_matcher.as_package_name() + if py_package_name is None: + return None + return PackageName._from_py_package_name(py_package_name) diff --git a/py-rattler/src/error.rs b/py-rattler/src/error.rs index 1c209f9d8..047c260f2 100644 --- a/py-rattler/src/error.rs +++ b/py-rattler/src/error.rs @@ -4,9 +4,9 @@ use pyo3::exceptions::PyValueError; use pyo3::{create_exception, exceptions::PyException, PyErr}; use rattler::install::TransactionError; use rattler_conda_types::{ - ConvertSubdirError, InvalidPackageNameError, ParseArchError, ParseChannelError, - ParseMatchSpecError, ParsePlatformError, ParseVersionError, ValidatePackageRecordsError, - VersionBumpError, VersionExtendError, + ConvertSubdirError, InvalidPackageNameError, PackageNameMatcherParseError, ParseArchError, + ParseChannelError, ParseMatchSpecError, ParsePlatformError, ParseVersionError, + ValidatePackageRecordsError, VersionBumpError, VersionExtendError, }; use rattler_lock::{ConversionError, ParseCondaLockError}; use rattler_networking::authentication_storage::AuthenticationStorageError; @@ -27,6 +27,8 @@ pub enum PyRattlerError { #[error(transparent)] InvalidPackageName(#[from] InvalidPackageNameError), #[error(transparent)] + PackageNameMatcherParseError(#[from] PackageNameMatcherParseError), + #[error(transparent)] InvalidUrl(#[from] url::ParseError), #[error(transparent)] InvalidChannel(#[from] ParseChannelError), @@ -111,6 +113,9 @@ impl From for PyErr { PyRattlerError::InvalidPackageName(err) => { InvalidPackageNameException::new_err(pretty_print_error(&err)) } + PyRattlerError::PackageNameMatcherParseError(err) => { + PackageNameMatcherParseException::new_err(pretty_print_error(&err)) + } PyRattlerError::InvalidUrl(err) => { InvalidUrlException::new_err(pretty_print_error(&err)) } @@ -196,6 +201,7 @@ impl From for PyErr { create_exception!(exceptions, InvalidVersionException, PyException); create_exception!(exceptions, InvalidMatchSpecException, PyException); create_exception!(exceptions, InvalidPackageNameException, PyException); +create_exception!(exceptions, PackageNameMatcherParseException, PyException); create_exception!(exceptions, InvalidUrlException, PyException); create_exception!(exceptions, InvalidChannelException, PyException); create_exception!(exceptions, ActivationException, PyException); diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index cc653d0af..f4461f3d5 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -12,6 +12,7 @@ mod nameless_match_spec; mod networking; mod no_arch_type; mod package_name; +mod package_name_matcher; mod package_streaming; mod paths_json; mod platform; @@ -35,9 +36,9 @@ use error::{ ActivationException, CacheDirException, ConvertSubdirException, DetectVirtualPackageException, EnvironmentCreationException, ExtractException, FetchRepoDataException, InvalidChannelException, InvalidMatchSpecException, InvalidPackageNameException, - InvalidUrlException, InvalidVersionException, IoException, LinkException, ParseArchException, - ParsePlatformException, PyRattlerError, SolverException, TransactionException, - ValidatePackageRecordsException, VersionBumpException, + InvalidUrlException, InvalidVersionException, IoException, LinkException, + PackageNameMatcherParseException, ParseArchException, ParsePlatformException, PyRattlerError, + SolverException, TransactionException, ValidatePackageRecordsException, VersionBumpException, }; use explicit_environment_spec::{PyExplicitEnvironmentEntry, PyExplicitEnvironmentSpec}; use generic_virtual_package::PyGenericVirtualPackage; @@ -58,6 +59,7 @@ use networking::middleware::{ use networking::{client::PyClientWithMiddleware, py_fetch_repo_data}; use no_arch_type::PyNoArchType; use package_name::PyPackageName; +use package_name_matcher::PyPackageNameMatcher; use paths_json::{PyFileMode, PyPathType, PyPathsEntry, PyPathsJson, PyPrefixPlaceholder}; use platform::{PyArch, PyPlatform}; use prefix_paths::{PyPrefixPathType, PyPrefixPaths, PyPrefixPathsEntry}; @@ -97,6 +99,7 @@ fn rattler<'py>(py: Python<'py>, m: Bound<'py, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -182,6 +185,10 @@ fn rattler<'py>(py: Python<'py>, m: Bound<'py, PyModule>) -> PyResult<()> { "InvalidMatchSpecError", py.get_type::(), )?; + m.add( + "PackageNameMatcherParseError", + py.get_type::(), + )?; m.add( "InvalidPackageNameError", py.get_type::(), diff --git a/py-rattler/src/match_spec.rs b/py-rattler/src/match_spec.rs index 8e669c7d5..787087630 100644 --- a/py-rattler/src/match_spec.rs +++ b/py-rattler/src/match_spec.rs @@ -1,12 +1,12 @@ -use std::borrow::Borrow; use std::sync::Arc; +use std::{borrow::Borrow, str::FromStr}; use pyo3::{pyclass, pymethods, types::PyBytes, Bound, PyResult, Python}; -use rattler_conda_types::{Channel, MatchSpec, Matches, PackageName, ParseStrictness}; +use rattler_conda_types::{Channel, MatchSpec, Matches, PackageNameMatcher, ParseStrictness}; use crate::{ channel::PyChannel, error::PyRattlerError, nameless_match_spec::PyNamelessMatchSpec, - package_name::PyPackageName, record::PyRecord, + package_name_matcher::PyPackageNameMatcher, record::PyRecord, }; #[pyclass] @@ -52,7 +52,7 @@ impl PyMatchSpec { /// The name of the package #[getter] - pub fn name(&self) -> Option { + pub fn name(&self) -> Option { self.inner.name.clone().map(std::convert::Into::into) } @@ -135,7 +135,7 @@ impl PyMatchSpec { Ok(Self { inner: MatchSpec::from_nameless( spec.clone().into(), - Some(PackageName::try_from(name).map_err(PyRattlerError::from)?), + Some(PackageNameMatcher::from_str(&name).map_err(PyRattlerError::from)?), ), }) } diff --git a/py-rattler/src/package_name.rs b/py-rattler/src/package_name.rs index b3af6b8fe..0e7e5e52e 100644 --- a/py-rattler/src/package_name.rs +++ b/py-rattler/src/package_name.rs @@ -10,7 +10,7 @@ use crate::error::PyRattlerError; #[pyclass] #[repr(transparent)] -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct PyPackageName { pub(crate) inner: PackageName, } diff --git a/py-rattler/src/package_name_matcher.rs b/py-rattler/src/package_name_matcher.rs new file mode 100644 index 000000000..b71e280eb --- /dev/null +++ b/py-rattler/src/package_name_matcher.rs @@ -0,0 +1,63 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + str::FromStr, +}; + +use pyo3::{pyclass, pymethods}; +use rattler_conda_types::{PackageName, PackageNameMatcher}; + +use crate::{error::PyRattlerError, package_name::PyPackageName}; + +#[pyclass] +#[derive(Clone)] +pub struct PyPackageNameMatcher { + pub(crate) inner: PackageNameMatcher, +} + +impl From for PackageNameMatcher { + fn from(value: PyPackageNameMatcher) -> Self { + value.inner + } +} + +impl From for PyPackageNameMatcher { + fn from(value: PackageNameMatcher) -> Self { + Self { inner: value } + } +} + +#[pymethods] +impl PyPackageNameMatcher { + /// Constructs a new `PackageNameMatcher` from a string, checking if the string is actually a + /// valid or normalized conda package name. + #[new] + pub fn new(source: String) -> pyo3::PyResult { + let inner = PackageNameMatcher::from_str(source.as_str()).map_err(PyRattlerError::from)?; + + Ok(Self { inner }) + } + + fn display_inner(&self) -> String { + match self.inner { + PackageNameMatcher::Exact(ref name) => { + format!("\"{}\", exact", name.as_source()) + } + PackageNameMatcher::Glob(ref glob) => format!("\"{glob}\", glob"), + PackageNameMatcher::Regex(ref regex) => { + format!("\"{regex}\", regex") + } + } + } + + fn as_package_name(&self) -> Option { + Option::::from(self.inner.clone()).map(PyPackageName::from) + } + + /// Compute the hash of the name. + fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.inner.hash(&mut hasher); + hasher.finish() + } +} diff --git a/py-rattler/src/solver.rs b/py-rattler/src/solver.rs index 2b2ea2931..5e246f243 100644 --- a/py-rattler/src/solver.rs +++ b/py-rattler/src/solver.rs @@ -4,6 +4,7 @@ use pyo3::{ FromPyObject, PyAny, PyErr, PyResult, Python, }; use pyo3_async_runtimes::tokio::future_into_py; +use rattler_conda_types::PackageName; use rattler_repodata_gateway::sparse::SparseRepoData; use rattler_solve::{resolvo::Solver, RepoDataIter, SolveStrategy, SolverImpl, SolverTask}; use tokio::task::JoinError; @@ -156,9 +157,13 @@ pub fn py_solve_with_sparse_repodata<'py>( }) .collect::, _>>()?; - let package_names = specs - .iter() - .filter_map(|match_spec| match_spec.inner.name.clone()); + let package_names = specs.iter().filter_map(|match_spec| { + match_spec + .inner + .name + .as_ref() + .and_then(|n| Option::::from(n.clone())) + }); let available_packages = SparseRepoData::load_records_recursive( repo_data_refs, diff --git a/py-rattler/tests/unit/test_matchspec.py b/py-rattler/tests/unit/test_matchspec.py index ff704aee0..e430f2bd1 100644 --- a/py-rattler/tests/unit/test_matchspec.py +++ b/py-rattler/tests/unit/test_matchspec.py @@ -65,7 +65,9 @@ def test_parse_no_channel() -> None: m = MatchSpec("python[version=3.9]") assert m.channel is None assert m.name is not None - assert m.name.normalized == "python" + package_name = m.name.as_package_name() + assert package_name is not None + assert package_name.normalized == "python" assert m.version == "==3.9"