diff --git a/libmamba/include/mamba/specs/match_spec.hpp b/libmamba/include/mamba/specs/match_spec.hpp index a63f1636a5..e3d6be77b6 100644 --- a/libmamba/include/mamba/specs/match_spec.hpp +++ b/libmamba/include/mamba/specs/match_spec.hpp @@ -12,11 +12,14 @@ #include #include +#include + #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; + using string_set = typename util::flat_set; + using string_set_const_ref = typename std::reference_wrapper; 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; @@ -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; + 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 diff --git a/libmamba/include/mamba/specs/unresolved_channel.hpp b/libmamba/include/mamba/specs/unresolved_channel.hpp index be92d09b92..9c4d58b97d 100644 --- a/libmamba/include/mamba/specs/unresolved_channel.hpp +++ b/libmamba/include/mamba/specs/unresolved_channel.hpp @@ -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: diff --git a/libmamba/include/mamba/specs/version_spec.hpp b/libmamba/include/mamba/specs/version_spec.hpp index 721d4b5b63..0587e5f3ec 100644 --- a/libmamba/include/mamba/specs/version_spec.hpp +++ b/libmamba/include/mamba/specs/version_spec.hpp @@ -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; diff --git a/libmamba/include/mamba/util/string.hpp b/libmamba/include/mamba/util/string.hpp index 2c6f04b014..c1c9c83d9f 100644 --- a/libmamba/include/mamba/util/string.hpp +++ b/libmamba/include/mamba/util/string.hpp @@ -228,6 +228,18 @@ namespace mamba::util [[nodiscard]] auto rsplit_once(std::string_view str, std::string_view sep) -> std::tuple, std::string_view>; + [[nodiscard]] auto split_once_on_any(std::string_view str, std::string_view many_seps) + -> std::tuple>; + template + [[nodiscard]] auto split_once_on_any(std::string_view str, std::array many_seps) + -> std::tuple>; + + [[nodiscard]] auto rsplit_once_on_any(std::string_view str, std::string_view many_seps) + -> std::tuple, std::string_view>; + template + [[nodiscard]] auto rsplit_once_on_any(std::string_view str, std::array many_seps) + -> std::tuple, std::string_view>; + [[nodiscard]] auto split(std::string_view input, std::string_view sep, std::size_t max_split = SIZE_MAX) -> std::vector; @@ -543,6 +555,20 @@ namespace mamba::util return detail::strip_if_parts_impl(input, std::move(should_strip)); } + template + auto split_once_on_any(std::string_view str, std::array many_seps) + -> std::tuple> + { + return split_once_on_any(str, std::string_view{ many_seps.data(), many_seps.size() }); + } + + template + auto rsplit_once_on_any(std::string_view str, std::array many_seps) + -> std::tuple, std::string_view> + { + return rsplit_once_on_any(str, std::string_view{ many_seps.data(), many_seps.size() }); + } + /************************************** * Implementation of join functions * **************************************/ diff --git a/libmamba/src/specs/match_spec.cpp b/libmamba/src/specs/match_spec.cpp index b04fb5ed85..3b09e47181 100644 --- a/libmamba/src/specs/match_spec.cpp +++ b/libmamba/src/specs/match_spec.cpp @@ -4,7 +4,6 @@ // // The full license is in the file LICENSE, distributed with this software. -#include #include #include #include @@ -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(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>> + { + // 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 { - 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(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 + { + return split_attribute_val(attrs.substr(0, next_pos)) + .and_then( + [&](auto&& key_val) + { + auto [key, value] = std::forward(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 { @@ -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 { 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 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; +} diff --git a/libmamba/src/specs/unresolved_channel.cpp b/libmamba/src/specs/unresolved_channel.cpp index 9c4f3c3b5f..2a8576f640 100644 --- a/libmamba/src/specs/unresolved_channel.cpp +++ b/libmamba/src/specs/unresolved_channel.cpp @@ -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); diff --git a/libmamba/src/specs/version_spec.cpp b/libmamba/src/specs/version_spec.cpp index 4a3a1d2dd3..2d5a997703 100644 --- a/libmamba/src/specs/version_spec.cpp +++ b/libmamba/src/specs/version_spec.cpp @@ -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 diff --git a/libmamba/src/util/string.cpp b/libmamba/src/util/string.cpp index 600d401de4..d6d78ab920 100644 --- a/libmamba/src/util/string.cpp +++ b/libmamba/src/util/string.cpp @@ -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> + { + 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::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 * ***************************************/ diff --git a/libmamba/tests/src/specs/test_match_spec.cpp b/libmamba/tests/src/specs/test_match_spec.cpp index 45f79417de..830e45bc96 100644 --- a/libmamba/tests/src/specs/test_match_spec.cpp +++ b/libmamba/tests/src/specs/test_match_spec.cpp @@ -7,6 +7,7 @@ #include #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") + SUBCASE("") { - auto ms = MatchSpec::parse("xtensor==0.12.3").value(); - CHECK_EQ(ms.version().str(), "==0.12.3"); - CHECK_EQ(ms.name().str(), "xtensor"); + auto ms = MatchSpec::parse("").value(); + 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("") + SUBCASE("xtensor==0.12.3") { - auto ms = MatchSpec::parse("").value(); - CHECK_EQ(ms.version().str(), "=*"); - CHECK_EQ(ms.name().str(), "*"); + 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 ") + 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"); } } diff --git a/libmamba/tests/src/util/test_string.cpp b/libmamba/tests/src/util/test_string.cpp index d9661d34dd..2360f5acee 100644 --- a/libmamba/tests/src/util/test_string.cpp +++ b/libmamba/tests/src/util/test_string.cpp @@ -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>; + + 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::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"; diff --git a/libmambapy/tests/test_specs.py b/libmambapy/tests/test_specs.py index a3b6ec350d..4dbdd0ddec 100644 --- a/libmambapy/tests/test_specs.py +++ b/libmambapy/tests/test_specs.py @@ -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