Refactor MatchSpec::str (#3215)

* Add split_once_on_any

* Split MatchSpec::track_features

* Add VersionSpec::expression_size

* Add UnresolvedChannel::is_package

* Refactor MatchSpec::str to use fmt

* Forbid version of in MatchSpec in attributes

* Document more Conda discrepencies
This commit is contained in:
Antoine Prouvost 2024-03-08 17:09:22 +01:00 committed by GitHub
parent a00ef35675
commit c54ab6ce7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 486 additions and 196 deletions

View File

@ -12,11 +12,14 @@
#include <string>
#include <string_view>
#include <fmt/core.h>
#include "mamba/specs/build_number_spec.hpp"
#include "mamba/specs/error.hpp"
#include "mamba/specs/glob_spec.hpp"
#include "mamba/specs/unresolved_channel.hpp"
#include "mamba/specs/version_spec.hpp"
#include "mamba/util/flat_set.hpp"
#include "mamba/util/heap_optional.hpp"
namespace mamba::specs
@ -29,6 +32,8 @@ namespace mamba::specs
using BuildStringSpec = GlobSpec;
using platform_set = typename UnresolvedChannel::platform_set;
using platform_set_const_ref = std::reference_wrapper<const platform_set>;
using string_set = typename util::flat_set<std::string>;
using string_set_const_ref = typename std::reference_wrapper<const string_set>;
inline static constexpr char url_md5_sep = '#';
inline static constexpr char prefered_list_open = '[';
@ -41,6 +46,7 @@ namespace mamba::specs
inline static constexpr char attribute_sep = ',';
inline static constexpr char attribute_assign = '=';
inline static constexpr auto package_version_sep = std::array{ ' ', '=', '<', '>', '~', '!' };
inline static constexpr auto feature_sep = std::array{ ' ', ',' };
[[nodiscard]] static auto parse(std::string_view spec) -> expected_parse_t<MatchSpec>;
@ -88,8 +94,8 @@ namespace mamba::specs
[[nodiscard]] auto features() const -> std::string_view;
void set_features(std::string val);
[[nodiscard]] auto track_features() const -> std::string_view;
void set_track_features(std::string val);
[[nodiscard]] auto track_features() const -> std::optional<string_set_const_ref>;
void set_track_features(string_set val);
[[nodiscard]] auto optional() const -> bool;
void set_optional(bool opt);
@ -112,7 +118,7 @@ namespace mamba::specs
std::string license = {};
std::string license_family = {};
std::string features = {};
std::string track_features = {};
string_set track_features = {};
bool optional = false;
};
@ -142,4 +148,12 @@ namespace mamba::specs
auto operator""_ms(const char* str, std::size_t len) -> MatchSpec;
}
}
template <>
struct fmt::formatter<::mamba::specs::MatchSpec>
{
auto parse(format_parse_context& ctx) -> decltype(ctx.begin());
auto format(const ::mamba::specs::MatchSpec& spec, format_context& ctx) -> decltype(ctx.out());
};
#endif

View File

@ -107,6 +107,8 @@ namespace mamba::specs
[[nodiscard]] auto platform_filters() && -> platform_set;
auto clear_platform_filters() -> platform_set;
[[nodiscard]] auto is_package() const -> bool;
[[nodiscard]] auto str() const -> std::string;
private:

View File

@ -185,6 +185,11 @@ namespace mamba::specs
*/
[[nodiscard]] auto contains(const Version& point) const -> bool;
/**
* Return the size of the boolean expression tree.
*/
[[nodiscard]] auto expression_size() const -> std::size_t;
private:
tree_type m_tree;

View File

@ -228,6 +228,18 @@ namespace mamba::util
[[nodiscard]] auto rsplit_once(std::string_view str, std::string_view sep)
-> std::tuple<std::optional<std::string_view>, std::string_view>;
[[nodiscard]] auto split_once_on_any(std::string_view str, std::string_view many_seps)
-> std::tuple<std::string_view, std::optional<std::string_view>>;
template <std::size_t N>
[[nodiscard]] auto split_once_on_any(std::string_view str, std::array<char, N> many_seps)
-> std::tuple<std::string_view, std::optional<std::string_view>>;
[[nodiscard]] auto rsplit_once_on_any(std::string_view str, std::string_view many_seps)
-> std::tuple<std::optional<std::string_view>, std::string_view>;
template <std::size_t N>
[[nodiscard]] auto rsplit_once_on_any(std::string_view str, std::array<char, N> many_seps)
-> std::tuple<std::optional<std::string_view>, std::string_view>;
[[nodiscard]] auto
split(std::string_view input, std::string_view sep, std::size_t max_split = SIZE_MAX)
-> std::vector<std::string>;
@ -543,6 +555,20 @@ namespace mamba::util
return detail::strip_if_parts_impl(input, std::move(should_strip));
}
template <std::size_t N>
auto split_once_on_any(std::string_view str, std::array<char, N> many_seps)
-> std::tuple<std::string_view, std::optional<std::string_view>>
{
return split_once_on_any(str, std::string_view{ many_seps.data(), many_seps.size() });
}
template <std::size_t N>
auto rsplit_once_on_any(std::string_view str, std::array<char, N> many_seps)
-> std::tuple<std::optional<std::string_view>, std::string_view>
{
return rsplit_once_on_any(str, std::string_view{ many_seps.data(), many_seps.size() });
}
/**************************************
* Implementation of join functions *
**************************************/

View File

@ -4,7 +4,6 @@
//
// The full license is in the file LICENSE, distributed with this software.
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
@ -184,6 +183,24 @@ namespace mamba::specs
return util::starts_with_any(str, std::array{ 'y', 'Y', 't', 'T', '1' });
}
auto split_features(std::string_view str) -> MatchSpec::string_set
{
auto out = MatchSpec::string_set();
auto feat = std::string_view();
auto rest = std::optional<std::string_view>(str);
while (rest.has_value())
{
std::tie(feat, rest) = util::split_once_on_any(rest.value(), MatchSpec::feature_sep);
feat = util::strip(feat);
if (!feat.empty())
{
out.insert(std::string(feat));
}
}
return out;
}
[[nodiscard]] auto set_single_matchspec_attribute_impl( //
MatchSpec& spec,
std::string_view attr,
@ -252,7 +269,7 @@ namespace mamba::specs
}
if (attr == "track_features")
{
spec.set_track_features(std::string(val));
spec.set_track_features(split_features(val));
return {};
}
if (attr == "optional")
@ -284,6 +301,31 @@ namespace mamba::specs
);
}
[[nodiscard]] auto split_attribute_val(std::string_view key_val)
-> expected_parse_t<std::tuple<std::string_view, std::optional<std::string_view>>>
{
// Forbid known ambiguity
if (util::starts_with(key_val, "version"))
{
const auto op_val = util::lstrip(key_val, "version");
if ( //
util::starts_with(op_val, "==") //
|| util::starts_with(op_val, "!=")
|| util::starts_with(op_val, "~=") //
|| util::starts_with(op_val, '>') //
|| util::starts_with(op_val, '<'))
{
return make_unexpected_parse(fmt::format(
R"(Implicit format "{}" is not allowed, use "version='{}'" instead.)",
key_val,
op_val
));
}
}
return { util::split_once(key_val, MatchSpec::attribute_assign) };
}
[[nodiscard]] auto set_matchspec_attributes( //
MatchSpec& spec,
std::string_view attrs
@ -291,21 +333,41 @@ namespace mamba::specs
{
return find_attribute_split(attrs)
.and_then(
[&](std::size_t next_pos)
[&](std::size_t next_pos) -> expected_parse_t<std::size_t>
{
auto [key, value] = util::split_once(
attrs.substr(0, next_pos),
MatchSpec::attribute_assign
);
return set_single_matchspec_attribute(
spec,
util::to_lower(util::strip(key)),
strip_whitespace_quotes(value.value_or("true"))
)
return split_attribute_val(attrs.substr(0, next_pos))
.and_then(
[&](auto&& key_val)
{
auto [key, value] = std::forward<decltype(key_val)>(key_val);
return set_single_matchspec_attribute(
spec,
util::to_lower(util::strip(key)),
strip_whitespace_quotes(value.value_or("true"))
);
}
)
.transform([&]() { return next_pos; });
}
)
.and_then(
[&](std::size_t next_pos) -> expected_parse_t<std::size_t>
{
return split_attribute_val(attrs.substr(0, next_pos))
.and_then(
[&](auto&& key_val)
{
auto [key, value] = std::forward<decltype(key_val)>(key_val);
return set_single_matchspec_attribute(
spec,
util::to_lower(util::strip(key)),
strip_whitespace_quotes(value.value_or("true"))
)
.transform([&]() { return next_pos; });
}
);
}
)
.and_then(
[&](std::size_t next_pos) -> expected_parse_t<void>
{
@ -529,9 +591,7 @@ namespace mamba::specs
{
if (const auto& chan = channel(); chan.has_value())
{
auto type = chan->type();
using Type = typename UnresolvedChannel::Type;
return (type == Type::PackageURL) || (type == Type::PackagePath);
return chan->is_package();
}
return false;
}
@ -801,18 +861,18 @@ namespace mamba::specs
}
}
auto MatchSpec::track_features() const -> std::string_view
auto MatchSpec::track_features() const -> std::optional<string_set_const_ref>
{
if (m_extra.has_value())
{
return m_extra->track_features;
}
return "";
return std::nullopt;
}
void MatchSpec::set_track_features(std::string val)
void MatchSpec::set_track_features(string_set val)
{
if (val != track_features()) // Avoid allocating extra to set the default value
if (!val.empty()) // Avoid allocating extra if empty
{
extra().track_features = std::move(val);
}
@ -853,69 +913,13 @@ namespace mamba::specs
return fmt::format("{}", m_name);
}
auto MatchSpec::str() const -> std::string
namespace
{
std::stringstream res;
// builder = []
// brackets = []
// channel_matcher = self._match_components.get('channel')
// if channel_matcher and channel_matcher.exact_value:
// builder.append(text_type(channel_matcher))
// elif channel_matcher and not channel_matcher.matches_all:
// brackets.append("channel=%s" % text_type(channel_matcher))
// subdir_matcher = self._match_components.get('subdir')
// if subdir_matcher:
// if channel_matcher and channel_matcher.exact_value:
// builder.append('/%s' % subdir_matcher)
// else:
// brackets.append("subdir=%s" % subdir_matcher)
// TODO change as attribute if complex URL, and has "url" if PackageUrl
if (m_channel.has_value())
{
res << fmt::format("{}::", *m_channel);
}
// TODO when namespaces are implemented!
// if (!ns.empty())
// {
// res << ns;
// res << ":";
// }
res << m_name.str();
std::vector<std::string> formatted_brackets;
auto is_complex_relation = [](const std::string& s)
{ return s.find_first_of("><$^|,") != s.npos; };
if (!m_version.is_explicitly_free())
{
auto ver = m_version.str();
if (is_complex_relation(ver)) // TODO do on VersionSpec
{
formatted_brackets.push_back(util::concat("version='", ver, "'"));
}
else
{
res << ver;
// version_exact = true;
}
}
if (!m_build_string.is_free())
{
if (m_build_string.is_exact())
{
res << "=" << m_build_string.str();
}
else
{
formatted_brackets.push_back(util::concat("build='", m_build_string.str(), '\''));
}
}
auto maybe_quote = [](std::string_view data) -> std::string_view
/**
* Find if the string needs a quote, and if so return it.
* Otherwise return the empty string.
*/
auto find_needed_quote(std::string_view data) -> std::string_view
{
if (auto pos = data.find_first_of(R"( =")"); pos != std::string_view::npos)
{
@ -927,53 +931,11 @@ namespace mamba::specs
}
return "";
};
}
if (const auto& num = build_number(); !num.is_explicitly_free())
{
formatted_brackets.push_back(util::concat("build_number=", num.str()));
}
if (const auto& tf = track_features(); !tf.empty())
{
const auto& q = maybe_quote(tf);
formatted_brackets.push_back(util::concat("track_features=", q, tf, q));
}
if (const auto& feats = features(); !feats.empty())
{
const auto& q = maybe_quote(feats);
formatted_brackets.push_back(util::concat("features=", q, feats, q));
}
else if (const auto& fn = filename(); !fn.empty() && !channel_is_file())
{
// No "fn" when we have a URL
const auto& q = maybe_quote(fn);
formatted_brackets.push_back(util::concat("fn=", q, fn, q));
}
if (const auto& hash = md5(); !hash.empty())
{
formatted_brackets.push_back(util::concat("md5=", hash));
}
if (const auto& hash = sha256(); !hash.empty())
{
formatted_brackets.push_back(util::concat("sha256=", hash));
}
if (const auto& l = license(); !l.empty())
{
formatted_brackets.push_back(util::concat("license=", l));
}
if (const auto& lf = license_family(); !lf.empty())
{
formatted_brackets.push_back(util::concat("license_family=", lf));
}
if (optional())
{
formatted_brackets.emplace_back("optional");
}
if (!formatted_brackets.empty())
{
res << "[" << util::join(",", formatted_brackets) << "]";
}
return res.str();
auto MatchSpec::str() const -> std::string
{
return fmt::format("{}", *this);
}
auto MatchSpec::is_simple() const -> bool
@ -1001,3 +963,159 @@ namespace mamba::specs
}
}
}
auto
fmt::formatter<::mamba::specs::MatchSpec>::parse(format_parse_context& ctx) -> decltype(ctx.begin())
{
// make sure that range is empty
if (ctx.begin() != ctx.end() && *ctx.begin() != '}')
{
throw fmt::format_error("Invalid format");
}
return ctx.begin();
}
auto
fmt::formatter<::mamba::specs::MatchSpec>::format(
const ::mamba::specs::MatchSpec& spec,
format_context& ctx
) -> decltype(ctx.out())
{
using MatchSpec = ::mamba::specs::MatchSpec;
auto out = ctx.out();
if (const auto& chan = spec.channel(); chan.has_value() && chan->is_package())
{
out = fmt::format_to(out, "{}", chan.value());
if (const auto& md5 = spec.md5(); !md5.empty())
{
out = fmt::format_to(out, "{}{}", MatchSpec::url_md5_sep, md5);
}
return out;
}
if (const auto& chan = spec.channel())
{
out = fmt::format_to(
out,
"{}{}{}{}",
chan.value(),
MatchSpec::channel_namespace_spec_sep,
spec.name_space(),
MatchSpec::channel_namespace_spec_sep
);
}
else if (auto ns = spec.name_space(); !ns.empty())
{
out = fmt::format_to(out, "{}{}", ns, MatchSpec::channel_namespace_spec_sep);
}
out = fmt::format_to(out, "{}", spec.name());
const bool is_complex_version = spec.version().expression_size() > 1;
const bool is_complex_build_string = !(
spec.build_string().is_exact() || spec.build_string().is_free()
);
// Any relation is complex, we'll write them all inside the attribute section.
// For package filename, we avoid writing the version and build string again as they are part
// of the url.
if (!is_complex_version && !is_complex_build_string)
{
if (!spec.build_string().is_free())
{
out = fmt::format_to(out, "{}={}", spec.version(), spec.build_string());
}
else if (!spec.version().is_explicitly_free())
{
out = fmt::format_to(out, "{}", spec.version());
}
}
bool bracket_written = false;
auto ensure_bracket_open_or_comma = [&]()
{
out = fmt::format_to(
out,
"{}",
bracket_written ? MatchSpec::attribute_sep : MatchSpec::prefered_list_open
);
bracket_written = true;
};
auto ensure_bracket_close = [&]()
{
if (bracket_written)
{
out = fmt::format_to(out, "{}", MatchSpec::prefered_list_close);
}
};
if (is_complex_version || is_complex_build_string)
{
if (const auto& ver = spec.version(); !ver.is_explicitly_free())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "version={0}{1}{0}", MatchSpec::prefered_quote, ver);
}
if (const auto& bs = spec.build_string(); !bs.is_free())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "build={0}{1}{0}", MatchSpec::prefered_quote, bs);
}
}
if (const auto& num = spec.build_number(); !num.is_explicitly_free())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "build_number={0}{1}{0}", MatchSpec::prefered_quote, num);
}
if (const auto& tf = spec.track_features(); tf.has_value() && !tf->get().empty())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(
out,
"track_features={0}{1}{0}",
MatchSpec::prefered_quote,
fmt::join(tf->get(), std::string_view(&MatchSpec::feature_sep.front(), 1))
);
}
if (const auto& feats = spec.features(); !feats.empty())
{
ensure_bracket_open_or_comma();
const auto& q = mamba::specs::find_needed_quote(feats);
out = fmt::format_to(out, "features={0}{1}{0}", q, feats);
}
if (const auto& fn = spec.filename(); !fn.empty())
{
ensure_bracket_open_or_comma();
const auto& q = mamba::specs::find_needed_quote(fn);
out = fmt::format_to(out, "fn={0}{1}{0}", q, fn);
}
if (const auto& hash = spec.md5(); !hash.empty())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "md5={}", hash);
}
if (const auto& hash = spec.sha256(); !hash.empty())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "sha256={}", hash);
}
if (const auto& license = spec.license(); !license.empty())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "license={}", license);
}
if (const auto& lf = spec.license_family(); !lf.empty())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "license_family={}", lf);
}
if (spec.optional())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "optional");
}
ensure_bracket_close();
return out;
}

View File

@ -227,6 +227,11 @@ namespace mamba::specs
return std::exchange(m_platform_filters, {});
}
auto UnresolvedChannel::is_package() const -> bool
{
return (type() == Type::PackageURL) || (type() == Type::PackagePath);
}
auto UnresolvedChannel::str() const -> std::string
{
return fmt::format("{}", *this);

View File

@ -311,6 +311,11 @@ namespace mamba::specs
return fmt::format("{:b}", *this);
}
auto VersionSpec::expression_size() const -> std::size_t
{
return m_tree.size();
}
namespace
{
template <typename Val, typename Range>

View File

@ -628,6 +628,32 @@ namespace mamba::util
return rsplit_once_impl(str, sep);
}
/***************************************************
* Implementation of split_once_on_any functions *
***************************************************/
auto split_once_on_any(std::string_view str, std::string_view many_seps)
-> std::tuple<std::string_view, std::optional<std::string_view>>
{
static constexpr auto npos = std::string_view::npos;
if (const auto pos = str.find_first_of(many_seps); pos != npos)
{
return { str.substr(0, pos), str.substr(pos + 1) };
}
return { str, std::nullopt };
}
auto rsplit_once_on_any(std::string_view str, std::string_view many_seps)
-> std::tuple<std::optional<std::string_view>, std::string_view>
{
static constexpr auto npos = std::string_view::npos;
if (const auto pos = str.find_last_of(many_seps); pos != npos)
{
return { str.substr(0, pos), str.substr(pos + 1) };
}
return { std::nullopt, str };
}
/***************************************
* Implementation of split functions *
***************************************/

View File

@ -7,6 +7,7 @@
#include <doctest/doctest.h>
#include "mamba/specs/match_spec.hpp"
#include "mamba/util/string.hpp"
using namespace mamba;
using namespace mamba::specs;
@ -17,39 +18,44 @@ TEST_SUITE("specs::match_spec")
TEST_CASE("parse")
{
SUBCASE("xtensor==0.12.3")
{
auto ms = MatchSpec::parse("xtensor==0.12.3").value();
CHECK_EQ(ms.version().str(), "==0.12.3");
CHECK_EQ(ms.name().str(), "xtensor");
}
SUBCASE("<empty>")
{
auto ms = MatchSpec::parse("").value();
CHECK_EQ(ms.version().str(), "=*");
CHECK_EQ(ms.name().str(), "*");
CHECK(ms.name().is_free());
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK(ms.build_number().is_explicitly_free());
CHECK_EQ(ms.str(), "*");
}
SUBCASE("ipykernel ")
SUBCASE("xtensor==0.12.3")
{
auto ms = MatchSpec::parse("xtensor==0.12.3").value();
CHECK_EQ(ms.name().str(), "xtensor");
CHECK_EQ(ms.version().str(), "==0.12.3");
CHECK_EQ(ms.str(), "xtensor==0.12.3");
}
SUBCASE("ipykernel")
{
auto ms = MatchSpec::parse("ipykernel").value();
CHECK_EQ(ms.version().str(), "=*");
CHECK_EQ(ms.name().str(), "ipykernel");
CHECK(ms.version().is_explicitly_free());
CHECK_EQ(ms.str(), "ipykernel");
}
SUBCASE("ipykernel ")
{
auto ms = MatchSpec::parse("ipykernel ").value();
CHECK_EQ(ms.version().str(), "=*");
CHECK_EQ(ms.name().str(), "ipykernel");
CHECK(ms.version().is_explicitly_free());
}
SUBCASE("numpy 1.7*")
{
auto ms = MatchSpec::parse("numpy 1.7*").value();
CHECK_EQ(ms.version().str(), "=1.7");
CHECK_EQ(ms.name().str(), "numpy");
CHECK_EQ(ms.version().str(), "=1.7");
CHECK_EQ(ms.conda_build_form(), "numpy 1.7.*");
CHECK_EQ(ms.str(), "numpy=1.7");
}
@ -58,8 +64,10 @@ TEST_SUITE("specs::match_spec")
{
auto ms = MatchSpec::parse("conda-forge:pypi:xtensor==0.12.3").value();
CHECK_EQ(ms.name().str(), "xtensor");
CHECK_EQ(ms.version().str(), "==0.12.3");
CHECK_EQ(ms.channel().value().str(), "conda-forge");
CHECK_EQ(ms.name_space(), "pypi");
CHECK_EQ(ms.str(), "conda-forge:pypi:xtensor==0.12.3");
}
SUBCASE("conda-forge/linux-64::xtensor==0.12.3")
@ -67,36 +75,47 @@ TEST_SUITE("specs::match_spec")
auto ms = MatchSpec::parse("numpy[version='1.7|1.8']").value();
CHECK_EQ(ms.name().str(), "numpy");
CHECK_EQ(ms.version().str(), "==1.7|==1.8");
CHECK_EQ(ms.str(), "numpy[version='==1.7|==1.8']");
CHECK_EQ(ms.str(), R"(numpy[version="==1.7|==1.8"])");
}
SUBCASE("conda-forge/linux-64::xtensor==0.12.3")
{
auto ms = MatchSpec::parse("conda-forge/linux-64::xtensor==0.12.3").value();
CHECK_EQ(ms.version().str(), "==0.12.3");
CHECK_EQ(ms.name().str(), "xtensor");
CHECK_EQ(ms.version().str(), "==0.12.3");
REQUIRE(ms.channel().has_value());
CHECK_EQ(ms.channel()->location(), "conda-forge");
CHECK_EQ(ms.channel()->platform_filters(), PlatformSet{ "linux-64" });
CHECK_EQ(ms.optional(), false);
CHECK_EQ(ms.platforms().value().get(), PlatformSet{ "linux-64" });
CHECK_EQ(ms.str(), "conda-forge[linux-64]::xtensor==0.12.3");
}
SUBCASE("conda-forge::foo[build=3](target=blarg,optional)")
SUBCASE("conda-forge::foo[build=bld](target=blarg,optional)")
{
auto ms = MatchSpec::parse("conda-forge::foo[build=3](target=blarg,optional)").value();
CHECK_EQ(ms.version().str(), "=*");
auto ms = MatchSpec::parse("conda-forge::foo[build=bld](target=blarg,optional)").value();
CHECK_EQ(ms.name().str(), "foo");
CHECK(ms.version().is_explicitly_free());
REQUIRE(ms.channel().has_value());
CHECK_EQ(ms.channel()->location(), "conda-forge");
CHECK_EQ(ms.build_string().str(), "3");
CHECK_EQ(ms.build_string().str(), "bld");
CHECK_EQ(ms.optional(), true);
CHECK_EQ(ms.str(), "conda-forge::foo=*=bld[optional]");
}
SUBCASE("python[build_number=3]")
{
auto ms = MatchSpec::parse("python[build_number=3]").value();
CHECK_EQ(ms.name().str(), "python");
CHECK_EQ(ms.version().str(), "=*");
CHECK_EQ(ms.build_number().str(), "=3");
CHECK_EQ(ms.str(), R"(python[build_number="=3"])");
}
SUBCASE(R"(blas[track_features="mkl avx"])")
{
auto ms = MatchSpec::parse(R"(blas[track_features="mkl avx"])").value();
CHECK_EQ(ms.name().str(), "blas");
CHECK_EQ(ms.track_features().value().get(), MatchSpec::string_set{ "avx", "mkl" });
CHECK_EQ(ms.str(), R"(blas[track_features="avx mkl"])");
}
SUBCASE("python[build_number='<=3']")
@ -104,16 +123,18 @@ TEST_SUITE("specs::match_spec")
auto ms = MatchSpec::parse("python[build_number='<=3']").value();
CHECK_EQ(ms.name().str(), "python");
CHECK_EQ(ms.build_number().str(), "<=3");
CHECK_EQ(ms.str(), R"(python[build_number="<=3"])");
}
SUBCASE("https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda#7dbaa197d7ba6032caf7ae7f32c1efa0"
)
{
auto ms = MatchSpec::parse(
"https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda"
"#7dbaa197d7ba6032caf7ae7f32c1efa0"
)
.value();
constexpr auto str = std::string_view{
"https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda"
"#7dbaa197d7ba6032caf7ae7f32c1efa0"
};
auto ms = MatchSpec::parse(str).value();
CHECK_EQ(ms.name().str(), "ncurses");
CHECK_EQ(ms.version().str(), "==6.4");
CHECK_EQ(ms.build_string().str(), "h59595ed_2");
@ -123,14 +144,15 @@ TEST_SUITE("specs::match_spec")
);
CHECK_EQ(ms.filename(), "ncurses-6.4-h59595ed_2.conda");
CHECK_EQ(ms.md5(), "7dbaa197d7ba6032caf7ae7f32c1efa0");
CHECK_EQ(ms.str(), str);
}
SUBCASE("https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2")
{
auto ms = MatchSpec::parse(
"https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2"
)
.value();
constexpr auto str = std::string_view{
"https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2"
};
auto ms = MatchSpec::parse(str).value();
CHECK_EQ(ms.name().str(), "_libgcc_mutex");
CHECK_EQ(ms.version().str(), "==0.1");
CHECK_EQ(ms.build_string().str(), "conda_forge");
@ -139,14 +161,15 @@ TEST_SUITE("specs::match_spec")
"https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2"
);
CHECK_EQ(ms.filename(), "_libgcc_mutex-0.1-conda_forge.tar.bz2");
CHECK_EQ(ms.str(), str);
}
SUBCASE("https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_13.tar.bz2")
{
auto ms = MatchSpec::parse(
"https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_13.tar.bz2"
)
.value();
constexpr auto str = std::string_view{
"https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_13.tar.bz2"
};
auto ms = MatchSpec::parse(str).value();
CHECK_EQ(ms.name().str(), "libgcc-ng");
CHECK_EQ(ms.version().str(), "==11.2.0");
CHECK_EQ(ms.build_string().str(), "h1d223b6_13");
@ -155,26 +178,28 @@ TEST_SUITE("specs::match_spec")
"https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_13.tar.bz2"
);
CHECK_EQ(ms.filename(), "libgcc-ng-11.2.0-h1d223b6_13.tar.bz2");
CHECK_EQ(ms.str(), str);
}
SUBCASE("https://conda.anaconda.org/conda-canary/linux-64/conda-4.3.21.post699+1dab973-py36h4a561cd_0.tar.bz2"
)
{
auto ms = MatchSpec::parse(
"https://conda.anaconda.org/conda-canary/linux-64/conda-4.3.21.post699+1dab973-py36h4a561cd_0.tar.bz2"
)
.value();
constexpr auto str = std::string_view{
"https://conda.anaconda.org/conda-canary/linux-64/conda-4.3.21.post699+1dab973-py36h4a561cd_0.tar.bz2"
};
auto ms = MatchSpec::parse(str).value();
CHECK_EQ(ms.name().str(), "conda");
CHECK_EQ(ms.version().str(), "==4.3.21.0post699+1dab973"); // Note the ``.0post``
CHECK_EQ(ms.build_string().str(), "py36h4a561cd_0");
CHECK_EQ(ms.str(), str);
}
SUBCASE("/home/randomguy/Downloads/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2")
{
auto ms = MatchSpec::parse(
"/home/randomguy/Downloads/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2"
)
.value();
constexpr auto str = std::string_view{
"/home/randomguy/Downloads/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2"
};
auto ms = MatchSpec::parse(str).value();
CHECK_EQ(ms.name().str(), "_libgcc_mutex");
CHECK_EQ(ms.version().str(), "==0.1");
CHECK_EQ(ms.build_string().str(), "conda_forge");
@ -183,6 +208,7 @@ TEST_SUITE("specs::match_spec")
"/home/randomguy/Downloads/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2"
);
CHECK_EQ(ms.filename(), "_libgcc_mutex-0.1-conda_forge.tar.bz2");
CHECK_EQ(ms.str(), str);
}
SUBCASE("xtensor[url=file:///home/wolfv/Downloads/xtensor-0.21.4-hc9558a2_0.tar.bz2]")
@ -196,6 +222,7 @@ TEST_SUITE("specs::match_spec")
ms.channel().value().str(),
"file:///home/wolfv/Downloads/xtensor-0.21.4-hc9558a2_0.tar.bz2"
);
CHECK_EQ(ms.str(), "file:///home/wolfv/Downloads/xtensor-0.21.4-hc9558a2_0.tar.bz2");
}
SUBCASE("foo=1.0=2")
@ -236,29 +263,32 @@ TEST_SUITE("specs::match_spec")
);
}
SUBCASE(R"(defaults::numpy=1.8=py27_0 [name="pytorch" channel='anaconda',version=">=1.8,<2|1.9", build='3'])"
SUBCASE(R"(defaults::numpy=1.8=py27_0 [name="pytorch",channel='anaconda',version=">=1.8,<2|1.9", build='3'])"
)
{
auto ms = MatchSpec::parse(
R"(defaults::numpy=1.8=py27_0 [name="pytorch" channel='anaconda',version=">=1.8,<2|1.9", build='3'])"
R"(defaults::numpy=1.8=py27_0 [name="pytorch",channel='anaconda',version=">=1.8,<2|1.9", build='3'])"
)
.value();
CHECK_EQ(ms.channel().value().str(), "defaults");
CHECK_EQ(ms.channel().value().str(), "anaconda");
CHECK_EQ(ms.name().str(), "numpy");
CHECK_EQ(ms.version().str(), "=1.8");
CHECK_EQ(ms.build_string().str(), "py27_0");
CHECK_EQ(ms.str(), R"(anaconda::numpy=1.8=py27_0)");
}
SUBCASE(R"(defaults::numpy [ "pytorch" channel='anaconda',version=">=1.8,<2|1.9", build='3'])")
SUBCASE(R"(defaults::numpy [ name="pytorch",channel='anaconda',version=">=1.8,<2|1.9", build='3'])"
)
{
auto ms = MatchSpec::parse(
R"(defaults::numpy [ "pytorch" channel='anaconda',version=">=1.8,<2|1.9", build='3'])"
R"(defaults::numpy [ name="pytorch",channel='anaconda',version=">=1.8,<2|1.9", build='3'])"
)
.value();
CHECK_EQ(ms.channel().value().str(), "defaults");
CHECK_EQ(ms.channel().value().str(), "anaconda");
CHECK_EQ(ms.name().str(), "numpy");
CHECK_EQ(ms.version().str(), ">=1.8,(<2|==1.9)");
CHECK_EQ(ms.build_string().str(), "3");
CHECK_EQ(ms.str(), R"ms(anaconda::numpy[version=">=1.8,(<2|==1.9)",build="3"])ms");
}
SUBCASE("numpy >1.8,<2|==1.7,!=1.9,~=1.7.1 py34_0")
@ -267,6 +297,7 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.name().str(), "numpy");
CHECK_EQ(ms.version().str(), ">1.8,((<2|==1.7),(!=1.9,~=1.7))");
CHECK_EQ(ms.build_string().str(), "py34_0");
CHECK_EQ(ms.str(), R"ms(numpy[version=">1.8,((<2|==1.7),(!=1.9,~=1.7))",build="py34_0"])ms");
}
SUBCASE("*[md5=fewjaflknd]")
@ -274,118 +305,120 @@ TEST_SUITE("specs::match_spec")
auto ms = MatchSpec::parse("*[md5=fewjaflknd]").value();
CHECK(ms.name().is_free());
CHECK_EQ(ms.md5(), "fewjaflknd");
CHECK_EQ(ms.str(), "*[md5=fewjaflknd]");
}
SUBCASE("libblas=*=*mkl")
{
auto ms = MatchSpec::parse("libblas=*=*mkl").value();
CHECK_EQ(ms.conda_build_form(), "libblas * *mkl");
CHECK_EQ(ms.name().str(), "libblas");
CHECK_EQ(ms.version().str(), "=*");
CHECK(ms.version().is_explicitly_free());
CHECK_EQ(ms.build_string().str(), "*mkl");
// CHECK_EQ(ms.str(), "foo==1.0=2");
CHECK_EQ(ms.str(), R"(libblas[build="*mkl"])");
CHECK_EQ(ms.conda_build_form(), "libblas * *mkl");
}
SUBCASE("libblas=0.15*")
{
// '*' is part of the version, not the glob
auto ms = MatchSpec::parse("libblas=0.15*").value();
CHECK_EQ(ms.conda_build_form(), "libblas 0.15*.*");
CHECK_EQ(ms.name().str(), "libblas");
CHECK_EQ(ms.version().str(), "=0.15*");
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "libblas=0.15*");
CHECK_EQ(ms.conda_build_form(), "libblas 0.15*.*");
}
SUBCASE("xtensor =0.15*")
{
// '*' is part of the version, not the glob
auto ms = MatchSpec::parse("xtensor =0.15*").value();
CHECK_EQ(ms.conda_build_form(), "xtensor 0.15*.*");
CHECK_EQ(ms.str(), "xtensor=0.15*");
CHECK_EQ(ms.name().str(), "xtensor");
CHECK_EQ(ms.version().str(), "=0.15*");
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "xtensor=0.15*");
CHECK_EQ(ms.conda_build_form(), "xtensor 0.15*.*");
}
SUBCASE("numpy=1.20")
{
auto ms = MatchSpec::parse("numpy=1.20").value();
CHECK_EQ(ms.str(), "numpy=1.20");
CHECK_EQ(ms.name().str(), "numpy");
CHECK_EQ(ms.version().str(), "=1.20");
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "numpy=1.20");
}
SUBCASE("conda-forge::tzdata")
{
auto ms = MatchSpec::parse("conda-forge::tzdata").value();
CHECK_EQ(ms.str(), "conda-forge::tzdata");
CHECK_EQ(ms.channel().value().str(), "conda-forge");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "conda-forge::tzdata");
}
SUBCASE("conda-forge/noarch::tzdata")
{
auto ms = MatchSpec::parse("conda-forge/noarch::tzdata").value();
CHECK_EQ(ms.str(), "conda-forge[noarch]::tzdata");
CHECK_EQ(ms.channel().value().str(), "conda-forge[noarch]");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "conda-forge[noarch]::tzdata");
}
SUBCASE("conda-forge[noarch]::tzdata")
{
auto ms = MatchSpec::parse("conda-forge/noarch::tzdata").value();
CHECK_EQ(ms.str(), "conda-forge[noarch]::tzdata");
CHECK_EQ(ms.channel().value().str(), "conda-forge[noarch]");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "conda-forge[noarch]::tzdata");
}
SUBCASE("pkgs/main::tzdata")
{
auto ms = MatchSpec::parse("pkgs/main::tzdata").value();
CHECK_EQ(ms.str(), "pkgs/main::tzdata");
CHECK_EQ(ms.channel().value().str(), "pkgs/main");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "pkgs/main::tzdata");
}
SUBCASE("pkgs/main/noarch::tzdata")
{
auto ms = MatchSpec::parse("pkgs/main/noarch::tzdata").value();
CHECK_EQ(ms.str(), "pkgs/main[noarch]::tzdata");
CHECK_EQ(ms.channel().value().str(), "pkgs/main[noarch]");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "pkgs/main[noarch]::tzdata");
}
SUBCASE("conda-forge[noarch]::tzdata[subdir=linux64]")
{
auto ms = MatchSpec::parse("conda-forge[noarch]::tzdata[subdir=linux64]").value();
CHECK_EQ(ms.str(), "conda-forge[noarch]::tzdata");
CHECK_EQ(ms.channel().value().str(), "conda-forge[noarch]");
CHECK_EQ(ms.platforms().value().get(), MatchSpec::platform_set{ "noarch" });
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "conda-forge[noarch]::tzdata");
}
SUBCASE("conda-forge::tzdata[subdir=mamba-37]")
{
auto ms = MatchSpec::parse("conda-forge::tzdata[subdir=mamba-37]").value();
CHECK_EQ(ms.str(), "conda-forge[mamba-37]::tzdata");
CHECK_EQ(ms.channel().value().str(), "conda-forge[mamba-37]");
CHECK_EQ(ms.platforms().value().get(), MatchSpec::platform_set{ "mamba-37" });
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK_EQ(ms.str(), "conda-forge[mamba-37]::tzdata");
}
SUBCASE("conda-canary/linux-64::conda==4.3.21.post699+1dab973=py36h4a561cd_0")
@ -399,6 +432,35 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.name().str(), "conda");
CHECK_EQ(ms.version().str(), "==4.3.21.0post699+1dab973"); // Not ``.0post`` diff
CHECK_EQ(ms.build_string().str(), "py36h4a561cd_0");
CHECK_EQ(ms.str(), "conda-canary[linux-64]::conda==4.3.21.0post699+1dab973=py36h4a561cd_0");
}
}
TEST_CASE("Conda discrepencies")
{
SUBCASE("python=3.7=bld")
{
// For some reason, conda parses version differently in `python=3.7` and
// `python=3.7=bld`.
// It is `=3.7` and `==3.7` in the later.
auto ms = MatchSpec::parse("python=3.7=bld").value();
CHECK_EQ(ms.version().str(), "=3.7");
CHECK_EQ(ms.build_string().str(), "bld");
}
SUBCASE("python[version>3]")
{
// Supported by conda but we consider to be already served by `version=">3"`
auto error = MatchSpec::parse("python[version>3]").error();
CHECK(util::contains(error.what(), R"(use "version='>3'" instead)"));
}
SUBCASE("python[version=3.7]")
{
// Ambiguous, `version=` parsed as attribute assignment, which leads to
// `3.7` (similar to `==3.7`) being parsed as VersionSpec
auto ms = MatchSpec::parse("python[version=3.7]").value();
CHECK_EQ(ms.version().str(), "==3.7");
}
}

View File

@ -402,6 +402,32 @@ namespace
CHECK_EQ(rsplit_once("hello//my/world", "//"), Out{ "hello", "my/world" });
}
TEST_CASE("split_once_on_any")
{
using Out = std::tuple<std::string_view, std::optional<std::string_view>>;
CHECK_EQ(split_once_on_any("", "/"), Out{ "", std::nullopt });
CHECK_EQ(split_once_on_any("hello,dear world", ", "), Out{ "hello", "dear world" });
CHECK_EQ(split_once_on_any("hello dear,world", ", "), Out{ "hello", "dear,world" });
CHECK_EQ(split_once_on_any("hello/world", "/"), Out{ "hello", "world" });
CHECK_EQ(split_once_on_any("hello//world", "//"), Out{ "hello", "/world" });
CHECK_EQ(split_once_on_any("hello/my//world", "/"), Out{ "hello", "my//world" });
CHECK_EQ(split_once_on_any("hello/my//world", "//"), Out{ "hello", "my//world" });
}
TEST_CASE("rsplit_once_on_any")
{
using Out = std::tuple<std::optional<std::string_view>, std::string_view>;
CHECK_EQ(rsplit_once_on_any("", "/"), Out{ std::nullopt, "" });
CHECK_EQ(rsplit_once_on_any("hello,dear world", ", "), Out{ "hello,dear", "world" });
CHECK_EQ(rsplit_once_on_any("hello dear,world", ", "), Out{ "hello dear", "world" });
CHECK_EQ(rsplit_once_on_any("hello/world", "/"), Out{ "hello", "world" });
CHECK_EQ(rsplit_once_on_any("hello//world", "//"), Out{ "hello/", "world" });
CHECK_EQ(rsplit_once_on_any("hello/my//world", "/"), Out{ "hello/my/", "world" });
CHECK_EQ(rsplit_once_on_any("hello/my//world", "//"), Out{ "hello/my/", "world" });
}
TEST_CASE("split")
{
std::string a = "hello.again.it's.me.mario";

View File

@ -853,15 +853,16 @@ def test_MatchSpec():
assert ms.sha256 == "s"
assert ms.license == "l"
assert ms.license_family == "lf"
assert ms.track_features == "ft"
assert ms.track_features == {"ft"}
assert ms.optional
assert not ms.is_file()
assert not ms.is_simple()
# str
assert str(ms) == (
"conda-forge[plat]::python=3.7"
"[build='*pypy',track_features=ft,md5=m,sha256=s,license=l,license_family=lf,optional]"
"conda-forge[plat]:ns:python"
"""[version="=3.7",build="*pypy",track_features="ft",md5=m,sha256=s,"""
"""license=l,license_family=lf,optional]"""
)
# Copy