Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: edge cases for matchspec parsing #217

Merged
merged 2 commits into from
Jun 14, 2023
Merged
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
2 changes: 1 addition & 1 deletion crates/rattler_conda_types/src/match_spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ use matcher::StringMatcher;
///
/// let spec = MatchSpec::from_str("foo=1.0=py27_0").unwrap();
/// assert_eq!(spec.name, Some("foo".to_string()));
/// assert_eq!(spec.version, Some(VersionSpec::from_str("1.0.*").unwrap()));
/// assert_eq!(spec.version, Some(VersionSpec::from_str("==1.0").unwrap()));
/// assert_eq!(spec.build, Some(StringMatcher::from_str("py27_0").unwrap()));
///
/// let spec = MatchSpec::from_str("conda-forge::foo[version=\"1.0.*\"]").unwrap();
Expand Down
82 changes: 61 additions & 21 deletions crates/rattler_conda_types/src/match_spec/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,30 @@ fn parse(input: &str) -> Result<MatchSpec, ParseMatchSpecError> {
Cow::Borrowed(version_str)
};

// Special case handling for version strings that start with `=`.
let version_str = if let (Some(version_str), true) =
(version_str.strip_prefix("=="), build_str.is_none())
{
// If the version starts with `==` and the build string is none we strip the `==` part.
Cow::Borrowed(version_str)
} else if let Some(version_str_part) = version_str.strip_prefix('=') {
let not_a_group = !version_str_part.contains(['=', ',', '|']);
if not_a_group {
// If the version starts with `=`, is not part of a group (e.g. 1|2) we append a *
// if it doesnt have one already.
if build_str.is_none() && !version_str_part.ends_with('*') {
Cow::Owned(format!("{version_str_part}*"))
} else {
Cow::Borrowed(version_str_part)
}
} else {
// Version string is part of a group, return the non-stripped version string
version_str
}
} else {
version_str
};

// Parse the version spec
match_spec.version = Some(
VersionSpec::from_str(version_str.as_ref())
Expand All @@ -389,6 +413,8 @@ fn parse(input: &str) -> Result<MatchSpec, ParseMatchSpecError> {

#[cfg(test)]
mod tests {
use serde::Serialize;
use std::collections::BTreeMap;
use std::str::FromStr;

use super::{
Expand Down Expand Up @@ -462,27 +488,6 @@ mod tests {
assert_eq!(split_version_and_build("* *"), Ok(("*", Some("*"))));
}

#[test]
fn test_match_spec() {
insta::assert_yaml_snapshot!([
MatchSpec::from_str("python 3.8.* *_cpython").unwrap(),
MatchSpec::from_str("foo=1.0=py27_0").unwrap(),
MatchSpec::from_str("foo==1.0=py27_0").unwrap(),
],
@r###"
---
- name: python
version: 3.8.*
build: "*_cpython"
- name: foo
version: 1.0.*
build: py27_0
- name: foo
version: "==1.0"
build: py27_0
"###);
}

#[test]
fn test_nameless_match_spec() {
insta::assert_yaml_snapshot!([
Expand Down Expand Up @@ -566,4 +571,39 @@ mod tests {
&[("version", "1.3,2.0")]
);
}

#[test]
fn test_from_str() {
// A list of matchspecs to parse.
// Please keep this list sorted.
let specs = [
"blas *.* mkl",
"foo=1.0=py27_0",
"foo==1.0=py27_0",
"python 3.8.* *_cpython",
"pytorch=*=cuda*",
];

#[derive(Serialize)]
#[serde(untagged)]
enum MatchSpecOrError {
Error { error: String },
MatchSpec(MatchSpec),
}

let evaluated: BTreeMap<_, _> = specs
.iter()
.map(|spec| {
(
spec,
MatchSpec::from_str(spec)
.map(MatchSpecOrError::MatchSpec)
.unwrap_or_else(|err| MatchSpecOrError::Error {
error: err.to_string(),
}),
)
})
.collect();
insta::assert_yaml_snapshot!("parsed matchspecs", evaluated);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: crates/rattler_conda_types/src/match_spec/parse.rs
expression: evaluated
---
blas *.* mkl:
name: blas
version: "*"
build: mkl
foo=1.0=py27_0:
name: foo
version: "==1.0"
build: py27_0
foo==1.0=py27_0:
name: foo
version: "==1.0"
build: py27_0
python 3.8.* *_cpython:
name: python
version: 3.8.*
build: "*_cpython"
pytorch=*=cuda*:
name: pytorch
version: "*"
build: cuda*

4 changes: 2 additions & 2 deletions crates/rattler_conda_types/src/version_spec/version_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ fn recognize_constraint<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
input: &'a str,
) -> Result<(&'a str, &'a str), nom::Err<E>> {
alt((
// Any
tag("*"),
// Any (* or *.*)
terminated(tag("*"), cut(opt(tag(".*")))),
// Regex
recognize(delimited(tag("^"), not(tag("$")), tag("$"))),
// Version with optional operator followed by optional glob.
Expand Down