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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/rattler/src/install/installer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<PackageName, Vec<String>> {
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)
Expand Down
3 changes: 2 additions & 1 deletion crates/rattler_conda_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
83 changes: 64 additions & 19 deletions crates/rattler_conda_types/src/match_spec/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Query language for conda packages.
use crate::package::ArchiveIdentifier;
use crate::{
build_spec::BuildNumberSpec, GenericVirtualPackage, PackageName, PackageRecord, RepoDataRecord,
Expand All @@ -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`].
Expand Down Expand Up @@ -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()));
/// ```
///
Expand All @@ -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<PackageName>,
pub name: Option<PackageNameMatcher>,
/// The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`)
pub version: Option<VersionSpec>,
/// The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`)
Expand Down Expand Up @@ -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, "*")?,
}

Expand Down Expand Up @@ -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<PackageName>, NamelessMatchSpec) {
pub fn into_nameless(self) -> (Option<PackageNameMatcher>, NamelessMatchSpec) {
(
self.name,
NamelessMatchSpec {
Expand All @@ -252,18 +258,21 @@ 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"^__"),
})
}
}

// Enable constructing a match spec from a package name.
impl From<PackageName> for MatchSpec {
fn from(value: PackageName) -> Self {
Self {
name: Some(value),
name: Some(PackageNameMatcher::Exact(value)),
..Default::default()
}
}
Expand Down Expand Up @@ -354,7 +363,7 @@ impl From<MatchSpec> for NamelessMatchSpec {

impl MatchSpec {
/// Constructs a [`MatchSpec`] from a [`NamelessMatchSpec`] and a name.
pub fn from_nameless(spec: NamelessMatchSpec, name: Option<PackageName>) -> Self {
pub fn from_nameless(spec: NamelessMatchSpec, name: Option<PackageNameMatcher>) -> Self {
Self {
name,
version: spec.version,
Expand Down Expand Up @@ -450,7 +459,7 @@ impl Matches<PackageRecord> 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;
}
}
Expand Down Expand Up @@ -533,7 +542,7 @@ impl Matches<GenericVirtualPackage> 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;
}
}
Expand Down Expand Up @@ -590,8 +599,8 @@ impl TryFrom<Url> 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)
Expand Down Expand Up @@ -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
}));
}
}
Loading