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: matchspec build / version from brackets and string serialization #917

Merged
merged 7 commits into from
Nov 1, 2024
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
101 changes: 96 additions & 5 deletions crates/rattler_conda_types/src/match_spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,23 @@ impl Display for MatchSpec {
let mut keys = Vec::new();

if let Some(md5) = &self.md5 {
keys.push(format!("md5={md5:x}"));
keys.push(format!("md5=\"{md5:x}\""));
}

if let Some(sha256) = &self.sha256 {
keys.push(format!("sha256={sha256:x}"));
keys.push(format!("sha256=\"{sha256:x}\""));
}

if let Some(build_number) = &self.build_number {
keys.push(format!("build_number=\"{build_number}\""));
}

if let Some(file_name) = &self.file_name {
keys.push(format!("fn=\"{file_name}\""));
}

if let Some(url) = &self.url {
keys.push(format!("url=\"{url}\""));
}

if !keys.is_empty() {
Expand Down Expand Up @@ -477,7 +489,7 @@ mod tests {

use crate::{
match_spec::Matches, MatchSpec, NamelessMatchSpec, PackageName, PackageRecord,
ParseStrictness::*, RepoDataRecord, Version,
ParseStrictness::*, RepoDataRecord, StringMatcher, Version,
};
use insta::assert_snapshot;
use std::hash::{Hash, Hasher};
Expand All @@ -493,7 +505,7 @@ mod tests {

#[test]
fn test_nameless_matchspec_format_eq() {
let spec = NamelessMatchSpec::from_str("*[version==1.0, sha256=aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97, md5=dede6252c964db3f3e41c7d30d07f6bf]", Strict).unwrap();
let spec = NamelessMatchSpec::from_str("*[version==1.0, sha256=aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97, md5=dede6252c964db3f3e41c7d30d07f6bf]", Lenient).unwrap();
let spec_as_string = spec.to_string();
let rebuild_spec = NamelessMatchSpec::from_str(&spec_as_string, Strict).unwrap();

Expand Down Expand Up @@ -544,7 +556,7 @@ mod tests {
..PackageRecord::new(
PackageName::new_unchecked("mamba"),
Version::from_str("1.0").unwrap(),
String::from(""),
String::from("foo_bar_py310_1"),
)
};

Expand Down Expand Up @@ -573,6 +585,85 @@ mod tests {

let spec = MatchSpec::from_str("mamba[version==1.0, md5=dede6252c964db3f3e41c7d30d07f6bf, sha256=aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97]", Strict).unwrap();
assert!(!spec.matches(&record));

let spec = MatchSpec::from_str("mamba[build=*py310_1]", Strict).unwrap();
assert!(spec.matches(&record));

let spec = MatchSpec::from_str("mamba[build=*py310*]", Strict).unwrap();
assert!(spec.matches(&record));

let spec = MatchSpec::from_str("mamba[build=*py39*]", Strict).unwrap();
assert!(!spec.matches(&record));

let spec = MatchSpec::from_str("mamba * [build=*py310*]", Strict).unwrap();
assert!(spec.matches(&record));

let spec = MatchSpec::from_str("mamba *[build=*py39*]", Strict).unwrap();
assert!(!spec.matches(&record));
assert!(spec.build == Some(StringMatcher::from_str("*py39*").unwrap()));

let spec = MatchSpec::from_str("mamba * [build=*py39*]", Strict).unwrap();
println!("Build: {:?}", spec.build);
assert!(!spec.matches(&record));
}

#[test]
fn precedence_version_build() {
let spec =
MatchSpec::from_str("foo 3.0.* [version=1.2.3, build='foobar']", Lenient).unwrap();
assert_eq!(spec.version.unwrap(), "1.2.3".parse().unwrap());
assert_eq!(spec.build.unwrap(), "foobar".parse().unwrap());

let spec = MatchSpec::from_str("foo 3.0.* abcdef[build='foobar', version=1.2.3]", Lenient)
.unwrap();
assert_eq!(spec.build.unwrap(), "foobar".parse().unwrap());
assert_eq!(spec.version.unwrap(), "1.2.3".parse().unwrap());

let spec =
NamelessMatchSpec::from_str("3.0.* [version=1.2.3, build='foobar']", Lenient).unwrap();
assert_eq!(spec.version.unwrap(), "1.2.3".parse().unwrap());
assert_eq!(spec.build.unwrap(), "foobar".parse().unwrap());

let spec =
NamelessMatchSpec::from_str("3.0.* abcdef[build='foobar', version=1.2.3]", Lenient)
.unwrap();
assert_eq!(spec.build.unwrap(), "foobar".parse().unwrap());
assert_eq!(spec.version.unwrap(), "1.2.3".parse().unwrap());
}

#[test]
fn strict_parsing_multiple_values() {
let spec = NamelessMatchSpec::from_str("3.0.* [version=1.2.3]", Strict);
assert!(spec.is_err());

let spec = NamelessMatchSpec::from_str("3.0.* foo[build='foobar']", Strict);
assert!(spec.is_err());

let spec = NamelessMatchSpec::from_str(
"3.0.* [build=baz, fn='/home/bla.tar.bz2' build='foobar']",
Strict,
);
assert!(spec.is_err());

let spec = MatchSpec::from_str("foo 3.0.* [version=1.2.3]", Strict);
assert!(spec.is_err());

let spec = MatchSpec::from_str("foo 3.0.* foo[build='foobar']", Strict);
assert!(spec.is_err());
assert!(spec
.unwrap_err()
.to_string()
.contains("multiple values for: build"));

let spec = MatchSpec::from_str(
"foo 3.0.* [build=baz, fn='/home/foo.tar.bz2', build='foobar']",
Strict,
);
assert!(spec.is_err());
assert!(spec
.unwrap_err()
.to_string()
.contains("multiple values for: build"));
}

#[test]
Expand Down
109 changes: 100 additions & 9 deletions crates/rattler_conda_types/src/match_spec/parse.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{borrow::Cow, ops::Not, str::FromStr, sync::Arc};
use std::{borrow::Cow, collections::HashSet, ops::Not, str::FromStr, sync::Arc};

use nom::{
branch::alt,
Expand Down Expand Up @@ -71,11 +71,11 @@ pub enum ParseMatchSpecError {
MultipleBracketSectionsNotAllowed,

/// Invalid version and build
#[error("Unable to parse version spec: {0}")]
#[error("unable to parse version spec: {0}")]
InvalidVersionAndBuild(String),

/// Invalid build string
#[error("The build string '{0}' is not valid, it can only contain alphanumeric characters and underscores"
#[error("the build string '{0}' is not valid, it can only contain alphanumeric characters and underscores"
)]
InvalidBuildString(String),

Expand All @@ -92,12 +92,16 @@ pub enum ParseMatchSpecError {
InvalidBuildNumber(#[from] ParseBuildNumberSpecError),

/// Unable to parse hash digest from hex
#[error("Unable to parse hash digest from hex")]
#[error("unable to parse hash digest from hex")]
InvalidHashDigest,

/// The package name was invalid
#[error(transparent)]
InvalidPackageName(#[from] InvalidPackageNameError),

/// Multiple values for a key in the matchspec
#[error("found multiple values for: {0}")]
MultipleValueForKey(String),
}

impl FromStr for MatchSpec {
Expand Down Expand Up @@ -227,6 +231,17 @@ fn parse_bracket_vec_into_components(
) -> Result<NamelessMatchSpec, ParseMatchSpecError> {
let mut match_spec = match_spec;

if strictness == Strict {
// check for duplicate keys
let mut seen = HashSet::new();
for (key, _) in &bracket {
if seen.contains(key) {
return Err(ParseMatchSpecError::MultipleValueForKey(key.to_string()));
}
seen.insert(key);
}
}

for elem in bracket {
let (key, value) = elem;
match key {
Expand Down Expand Up @@ -482,8 +497,19 @@ impl NamelessMatchSpec {
// Get the version and optional build string
if !input.is_empty() {
let (version, build) = parse_version_and_build(input, strictness)?;
match_spec.version = version;
match_spec.build = build;
if strictness == Strict {
if match_spec.version.is_some() && version.is_some() {
return Err(ParseMatchSpecError::MultipleValueForKey(
"version".to_owned(),
));
}

if match_spec.build.is_some() && build.is_some() {
return Err(ParseMatchSpecError::MultipleValueForKey("build".to_owned()));
}
}
match_spec.version = match_spec.version.or(version);
match_spec.build = match_spec.build.or(build);
}

Ok(match_spec)
Expand Down Expand Up @@ -578,8 +604,19 @@ fn matchspec_parser(
let input = input.trim();
if !input.is_empty() {
let (version, build) = parse_version_and_build(input, strictness)?;
match_spec.version = version;
match_spec.build = build;
if strictness == Strict {
if match_spec.version.is_some() && version.is_some() {
return Err(ParseMatchSpecError::MultipleValueForKey(
"version".to_owned(),
));
}

if match_spec.build.is_some() && build.is_some() {
return Err(ParseMatchSpecError::MultipleValueForKey("build".to_owned()));
}
}
match_spec.version = match_spec.version.or(version);
match_spec.build = match_spec.build.or(build);
}

Ok(match_spec)
Expand Down Expand Up @@ -772,9 +809,11 @@ mod tests {
NamelessMatchSpec::from_str("*cpu*", Strict).unwrap(),
NamelessMatchSpec::from_str("conda-forge::foobar", Strict).unwrap(),
NamelessMatchSpec::from_str("foobar[channel=conda-forge]", Strict).unwrap(),
NamelessMatchSpec::from_str("* [build=foo]", Strict).unwrap(),
NamelessMatchSpec::from_str(">=1.2[build=foo]", Strict).unwrap(),
NamelessMatchSpec::from_str("[version='>=1.2', build=foo]", Strict).unwrap(),
],
@r###"
---
- version: 3.8.*
build: "*_cpython"
- version: "==1.0"
Expand All @@ -792,6 +831,12 @@ mod tests {
channel:
base_url: "https://conda.anaconda.org/conda-forge/"
name: conda-forge
- version: "*"
build: foo
- version: ">=1.2"
build: foo
- version: ">=1.2"
build: foo
"###);
}

Expand Down Expand Up @@ -1279,4 +1324,50 @@ mod tests {
assert_eq!(subdir, expected_subdir.map(ToString::to_string));
}
}

#[test]
fn test_matchspec_to_string() {
let mut specs: Vec<MatchSpec> =
vec![MatchSpec::from_str("foo[version=1.0.*, build_number=\">6\"]", Strict).unwrap()];

// complete matchspec to verify that we print all fields
specs.push(MatchSpec {
name: Some("foo".parse().unwrap()),
version: Some(VersionSpec::from_str("1.0.*", Strict).unwrap()),
build: "py27_0*".parse().ok(),
build_number: Some(BuildNumberSpec::from_str(">=6").unwrap()),
file_name: Some("foo-1.0-py27_0.tar.bz2".to_string()),
channel: Some(
Channel::from_str("conda-forge", &channel_config())
.map(Arc::new)
.unwrap(),
),
subdir: Some("linux-64".to_string()),
namespace: Some("foospace".to_string()),
md5: Some(parse_digest_from_hex::<Md5>("8b1a9953c4611296a827abf8c47804d7").unwrap()),
sha256: Some(
parse_digest_from_hex::<Sha256>(
"315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3",
)
.unwrap(),
),
url: Some(
Url::parse(
"https://conda.anaconda.org/conda-forge/linux-64/foo-1.0-py27_0.tar.bz2",
)
.unwrap(),
),
});

// insta check all the strings
let vec_strings = specs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
insta::assert_debug_snapshot!(vec_strings);

// parse back the strings and check if they are the same
let parsed_specs = vec_strings
.iter()
.map(|s| MatchSpec::from_str(s, Strict).unwrap())
.collect::<Vec<_>>();
assert_eq!(specs, parsed_specs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/rattler_conda_types/src/match_spec/parse.rs
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\"]",
]
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ blas *.* mkl:
name: foo
url: "file:///C:/Users/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2"
foo=1.0=py27_0:
error: "The build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores"
error: "the build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores"
foo==1.0=py27_0:
error: "The build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores"
error: "the build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores"
"https://conda.anaconda.org/conda-forge/linux-64/py-rattler-0.6.1-py39h8169da8_0.conda":
name: py-rattler
url: "https://conda.anaconda.org/conda-forge/linux-64/py-rattler-0.6.1-py39h8169da8_0.conda"
Expand All @@ -24,7 +24,7 @@ python 3.8.* *_cpython:
version: 3.8.*
build: "*_cpython"
pytorch=*=cuda*:
error: "The build string '=cuda*' is not valid, it can only contain alphanumeric characters and underscores"
error: "the build string '=cuda*' is not valid, it can only contain alphanumeric characters and underscores"
"x264 >=1!164.3095,<1!165":
name: x264
version: ">=1!164.3095,<1!165"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ expression: evaluated
"C:\\Users\\user\\conda-bld\\linux-64\\foo-1.0-py27_0.tar.bz2":
url: "file:///C:/Users/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2"
"=1.0=py27_0":
error: "The build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores"
error: "the build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores"
"==1.0=py27_0":
error: "The build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores"
error: "the build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores"
"https://conda.anaconda.org/conda-forge/linux-64/py-rattler-0.6.1-py39h8169da8_0.conda":
url: "https://conda.anaconda.org/conda-forge/linux-64/py-rattler-0.6.1-py39h8169da8_0.conda"
"https://repo.prefix.dev/ruben-arts/linux-64/boost-cpp-1.78.0-h75c5d50_1.tar.bz2":
Expand All @@ -25,7 +25,7 @@ expression: evaluated
version: 3.8.*
build: "*_cpython"
"=*=cuda*":
error: "The build string '=cuda*' is not valid, it can only contain alphanumeric characters and underscores"
error: "the build string '=cuda*' is not valid, it can only contain alphanumeric characters and underscores"
">=1!164.3095,<1!165":
version: ">=1!164.3095,<1!165"
/home/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
source: crates/rattler_conda_types/src/match_spec/mod.rs
expression: "specs.into_iter().map(|s|\n MatchSpec::from_str(s).unwrap()).map(|s|\n s.to_string()).collect::<Vec<String>>().join(\"\\n\")"
expression: "specs.into_iter().map(|s|\nMatchSpec::from_str(s,\nStrict).unwrap()).map(|s| s.to_string()).collect::<Vec<String>>().join(\"\\n\")"
---
mamba ==1.0 py37_0
conda-forge::pytest ==1.0[md5=dede6252c964db3f3e41c7d30d07f6bf, sha256=aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97]
conda-forge::pytest ==1.0[md5="dede6252c964db3f3e41c7d30d07f6bf", sha256="aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97"]
conda-forge/linux-64::pytest
conda-forge/linux-64::pytest ==1.0
conda-forge/linux-64::pytest ==1.0 py37_0
Expand Down