diff --git a/libmamba/include/mamba/specs/match_spec.hpp b/libmamba/include/mamba/specs/match_spec.hpp index a7269d162..8b3fa5784 100644 --- a/libmamba/include/mamba/specs/match_spec.hpp +++ b/libmamba/include/mamba/specs/match_spec.hpp @@ -14,6 +14,7 @@ #include #include "mamba/specs/channel_spec.hpp" +#include "mamba/specs/version_spec.hpp" namespace mamba::specs { @@ -37,8 +38,8 @@ namespace mamba::specs [[nodiscard]] auto name() const -> const std::string&; void set_name(std::string name); - [[nodiscard]] auto version() const -> const std::string&; - void set_version(std::string ver); + [[nodiscard]] auto version() const -> const VersionSpec&; + void set_version(VersionSpec ver); [[nodiscard]] auto build_number() const -> const std::string&; void set_build_number(std::string num); @@ -67,9 +68,9 @@ namespace mamba::specs private: std::optional m_channel; + VersionSpec m_version; std::string m_name_space; std::string m_name; - std::string m_version; std::string m_build_number; std::string m_build_string; // TODO can put inside channel spec diff --git a/libmamba/include/mamba/specs/version_spec.hpp b/libmamba/include/mamba/specs/version_spec.hpp index c37c36ff2..b5dbc9c9d 100644 --- a/libmamba/include/mamba/specs/version_spec.hpp +++ b/libmamba/include/mamba/specs/version_spec.hpp @@ -48,6 +48,13 @@ namespace mamba::specs [[nodiscard]] auto str() const -> std::string; + /** + * An alternative string representation of the version spec. + * + * Attempts to be compatible with conda-build/libsolv. + */ + [[nodiscard]] auto str_conda_build() const -> std::string; + private: struct free_interval @@ -148,6 +155,15 @@ namespace mamba::specs VersionSpec() = default; explicit VersionSpec(tree_type&& tree) noexcept; + /** + * Returns wether the VersionSpec is unconstrained. + * + * Due to the complex nature of VersionSpec expressions, it is not always easy to know + * whether a complex expression can be simpified to the unconstrained one. + * This functions only handles the trivial cases. + */ + [[nodiscard]] auto is_explicitly_free() const -> bool; + /** * A string representation of the version spec. * @@ -156,6 +172,13 @@ namespace mamba::specs */ [[nodiscard]] auto str() const -> std::string; + /** + * An alternative string representation of the version spec. + * + * Attempts to be compatible with conda-build/libsolv. + */ + [[nodiscard]] auto str_conda_build() const -> std::string; + /** * True if the set described by the VersionSpec contains the given version. */ @@ -177,6 +200,11 @@ namespace mamba::specs template <> struct fmt::formatter { + /** + * Change the representation of some predicates not understood by conda-build/libsolv. + */ + bool conda_build_form = false; + auto parse(format_parse_context& ctx) -> decltype(ctx.begin()); auto format(const ::mamba::specs::VersionPredicate& pred, format_context& ctx) @@ -186,6 +214,11 @@ struct fmt::formatter template <> struct fmt::formatter { + /** + * Change the representation of some predicates not understood by conda-build/libsolv. + */ + bool conda_build_form = false; + auto parse(format_parse_context& ctx) -> decltype(ctx.begin()); auto format(const ::mamba::specs::VersionSpec& spec, format_context& ctx) -> decltype(ctx.out()); diff --git a/libmamba/src/api/install.cpp b/libmamba/src/api/install.cpp index e3ca06ea8..59d32d649 100644 --- a/libmamba/src/api/install.cpp +++ b/libmamba/src/api/install.cpp @@ -358,7 +358,7 @@ namespace mamba PackageInfo p(ms.name()); p.url = ms.url(); p.build_string = ms.build_string(); - p.version = ms.version(); + p.version = ms.version().str(); if (ms.channel().has_value()) { p.channel = ms.channel()->location(); diff --git a/libmamba/src/core/satisfiability_error.cpp b/libmamba/src/core/satisfiability_error.cpp index a815b4ed6..67379d116 100644 --- a/libmamba/src/core/satisfiability_error.cpp +++ b/libmamba/src/core/satisfiability_error.cpp @@ -148,7 +148,7 @@ namespace mamba std::vector groups{}; - std::size_t const n_nodes = node_indices.size(); + const std::size_t n_nodes = node_indices.size(); std::vector node_added_to_a_group(n_nodes, false); for (std::size_t i = 0; i < n_nodes; ++i) { @@ -538,14 +538,45 @@ namespace mamba * Implementation of CompressedProblemsGraph::RoughCompare * *************************************************************/ + namespace + { + template + auto invoke_name(T&& e) -> decltype(auto) + { + using TT = std::remove_cv_t>; + return std::invoke(&TT::name, std::forward(e)); + } + + template + auto invoke_version(T&& e) -> decltype(auto) + { + using TT = std::remove_cv_t>; + using Ver = decltype(std::invoke(&TT::version, std::forward(e))); + Ver v = std::invoke(&TT::version, std::forward(e)); + if constexpr (std::is_same_v, specs::VersionSpec>) + { + return std::forward(v).str(); + } + else + { + return v; + } + } + } + template - bool CompressedProblemsGraph::RoughCompare::operator()(const T& a, const T& b) const + auto CompressedProblemsGraph::RoughCompare::operator()(const T& a, const T& b) const -> bool { auto attrs = [](const auto& x) { - return std::tie( - std::invoke(&T::name, x), - std::invoke(&T::version, x), + using Attrs = std::tuple< + decltype(invoke_name(x)), + decltype(invoke_version(x)), + decltype(std::invoke(&T::build_number, x)), + decltype(std::invoke(&T::build_string, x))>; + return Attrs( + invoke_name(x), + invoke_version(x), std::invoke(&T::build_number, x), std::invoke(&T::build_string, x) ); @@ -562,16 +593,6 @@ namespace mamba * Implementation of CompressedProblemsGraph::NamedList * **********************************************************/ - namespace - { - template - decltype(auto) invoke_name(T&& e) - { - using TT = std::remove_cv_t>; - return std::invoke(&TT::name, std::forward(e)); - } - } - template template CompressedProblemsGraph::NamedList::NamedList(InputIterator first, InputIterator last) @@ -650,13 +671,13 @@ namespace mamba ) const -> std::pair { auto versions = std::vector(size()); - auto invoke_version = [](auto&& v) -> decltype(auto) - { - using TT = std::remove_cv_t>; - return std::invoke(&TT::version, std::forward(v)); - }; // TODO(C++20) *this | std::ranges::transform(invoke_version) | ranges::unique - std::transform(begin(), end(), versions.begin(), invoke_version); + std::transform( + begin(), + end(), + versions.begin(), + [](const auto& x) { return invoke_version(x); } + ); if (remove_duplicates) { versions.erase(std::unique(versions.begin(), versions.end()), versions.end()); @@ -1036,7 +1057,7 @@ namespace mamba const TreeNodeIter children_begin = out; // TODO(C++20) an enumerate view ``views::zip(views::iota(), children_ids)`` - std::size_t const n_children = children_ids.size(); + const std::size_t n_children = children_ids.size(); for (std::size_t i = 0; i < n_children; ++i) { const bool last = (i == n_children - 1); @@ -1228,7 +1249,7 @@ namespace mamba void TreeExplainer::write_ancestry(const std::vector& ancestry) { - std::size_t const size = ancestry.size(); + const std::size_t size = ancestry.size(); const auto indents = m_format.indents; if (size > 0) { @@ -1434,7 +1455,7 @@ namespace mamba void TreeExplainer::write_path(const std::vector& path) { - std::size_t const length = path.size(); + const std::size_t length = path.size(); for (std::size_t i = 0; i < length; ++i) { const bool last = (i == length - 1); diff --git a/libmamba/src/core/solver.cpp b/libmamba/src/core/solver.cpp index 1b83247e8..7092a8693 100644 --- a/libmamba/src/core/solver.cpp +++ b/libmamba/src/core/solver.cpp @@ -96,7 +96,8 @@ namespace mamba return m_jobs->push_back(job_flag | SOLVER_SOLVABLE_PROVIDES, m_pool.matchspec2id(ms)); } - if (ms.channel().has_value() || !ms.version().empty() || !ms.build_string().empty()) + if (ms.channel().has_value() || !ms.version().is_explicitly_free() + || !ms.build_string().empty()) { Console::stream() << ms.conda_build_form() << ": overriding channel, version and build from " @@ -105,7 +106,7 @@ namespace mamba auto ms_modified = ms; ms_modified.set_channel(specs::ChannelSpec::parse(solvable->channel())); - ms_modified.set_version(std::string(solvable->version())); + ms_modified.set_version(specs::VersionSpec::parse(solvable->version())); ms_modified.set_build_string(std::string(solvable->build_string())); LOG_INFO << "Reinstall " << ms_modified.conda_build_form() << " from channel " diff --git a/libmamba/src/core/transaction.cpp b/libmamba/src/core/transaction.cpp index 65d426607..a096b0d05 100644 --- a/libmamba/src/core/transaction.cpp +++ b/libmamba/src/core/transaction.cpp @@ -73,7 +73,7 @@ namespace mamba auto& p = out.back(); p.url = ms.url(); p.build_string = ms.build_string(); - p.version = ms.version(); + p.version = ms.version().str_conda_build(); if (ms.channel().has_value()) { p.channel = ms.channel()->location(); diff --git a/libmamba/src/specs/match_spec.cpp b/libmamba/src/specs/match_spec.cpp index a86657142..2cf0220b1 100644 --- a/libmamba/src/specs/match_spec.cpp +++ b/libmamba/src/specs/match_spec.cpp @@ -77,7 +77,7 @@ namespace mamba::specs // Version std::tie(head, tail) = util::rsplit_once(head.value(), '-'); - out.m_version = tail; + out.m_version = VersionSpec::parse(tail); if (!head.has_value()) { fail_parse(); @@ -192,12 +192,14 @@ namespace mamba::specs spec_str.push_back('*'); } // This is #6 of the spec parsing + // Look for version *and* build string and separator + auto version_and_build = std::string(); static std::regex version_build_re("([^ =<>!~]+)?([>= 2 && out.m_version[0] == '=') - { - auto rest = out.m_version.substr(1); - if (out.m_version[1] == '=' && out.m_build_string.empty()) - { - out.m_version = out.m_version.substr(2); - } - else if (rest.find_first_of("=,|") == rest.npos) - { - if (out.m_build_string.empty() && out.m_version.back() != '*') - { - out.m_version = util::concat(out.m_version, "*"); - } - else - { - out.m_version = rest; - } - } - } } - else + else // no-op { - out.m_version = ""; - out.m_build_string = ""; + out.m_version = {}; + out.m_build_string = {}; } // TODO think about using a hash function here, (and elsewhere), like: @@ -270,7 +249,7 @@ namespace mamba::specs } else if (k == "version") { - out.m_version = v; + out.m_version = VersionSpec::parse(v); } else if (k == "channel") { @@ -351,12 +330,12 @@ namespace mamba::specs m_name = std::move(name); } - auto MatchSpec::version() const -> const std::string& + auto MatchSpec::version() const -> const VersionSpec& { return m_version; } - void MatchSpec::set_version(std::string ver) + void MatchSpec::set_version(VersionSpec ver) { m_version = std::move(ver); } @@ -405,14 +384,17 @@ namespace mamba::specs { std::stringstream res; res << m_name; - if (!m_version.empty()) + if (!m_version.is_explicitly_free()) { - res << " " << m_version; - // if (!build.empty() && (build != "*")) - if (!m_build_string.empty()) - { - res << " " << m_build_string; - } + res << " " << m_version.str_conda_build(); + } + else if (!m_build_string.empty()) + { + res << " *"; + } + if (!m_build_string.empty()) + { + res << " " << m_build_string; } return res.str(); } @@ -453,50 +435,16 @@ namespace mamba::specs auto is_complex_relation = [](const std::string& s) { return s.find_first_of("><$^|,") != s.npos; }; - if (!m_version.empty()) + if (!m_version.is_explicitly_free()) { - if (is_complex_relation(m_version)) + auto ver = m_version.str(); + if (is_complex_relation(ver)) // TODO do on VersionSpec { - formatted_brackets.push_back(util::concat("version='", m_version, "'")); - } - else if (util::starts_with(m_version, "!=") || util::starts_with(m_version, "~=")) - { - if (!m_build_string.empty()) - { - formatted_brackets.push_back(util::concat("version='", m_version, "'")); - } - else - { - res << " " << m_version; - } - } - else if (util::ends_with(m_version, ".*")) - { - res << "=" + m_version.substr(0, m_version.size() - 2); - } - else if (m_version.back() == '*') - { - if (m_version.size() == 1) - { - res << "=*"; - } - else if (util::starts_with(m_version, "=")) - { - res << m_version.substr(0, m_version.size() - 1); - } - else - { - res << "=" + m_version.substr(0, m_version.size() - 1); - } - } - else if (util::starts_with(m_version, "==")) - { - res << m_version; - version_exact = true; + formatted_brackets.push_back(util::concat("version='", ver, "'")); } else { - res << "==" << m_version; + res << ver; version_exact = true; } } @@ -566,7 +514,7 @@ namespace mamba::specs auto MatchSpec::is_simple() const -> bool { - return m_version.empty() && m_build_string.empty() && m_build_number.empty(); + return m_version.is_explicitly_free() && m_build_string.empty() && m_build_number.empty(); } auto MatchSpec::is_file() const -> bool diff --git a/libmamba/src/specs/version_spec.cpp b/libmamba/src/specs/version_spec.cpp index 85ebd99e4..d395e7a35 100644 --- a/libmamba/src/specs/version_spec.cpp +++ b/libmamba/src/specs/version_spec.cpp @@ -160,6 +160,11 @@ namespace mamba::specs return fmt::format("{}", *this); } + auto VersionPredicate::str_conda_build() const -> std::string + { + return fmt::format("{:b}", *this); + } + VersionPredicate::VersionPredicate(Version ver, BinaryOperator op) : m_version(std::move(ver)) , m_operator(std::move(op)) @@ -181,10 +186,10 @@ auto fmt::formatter::parse(format_parse_context& ctx) -> decltype(ctx.begin()) { - // make sure that range is empty - if (ctx.begin() != ctx.end() && *ctx.begin() != '}') + if (auto it = std::find(ctx.begin(), ctx.end(), 'b'); it < ctx.end()) { - throw fmt::format_error("Invalid format"); + conda_build_form = true; + return ++it; } return ctx.begin(); } @@ -239,7 +244,14 @@ fmt::formatter::format( } if constexpr (std::is_same_v) { - out = fmt::format_to(out, "{}{}", VersionSpec::starts_with_str, pred.m_version); + if (conda_build_form) + { + out = fmt::format_to(out, "{}{}", pred.m_version, VersionSpec::glob_suffix_str); + } + else + { + out = fmt::format_to(out, "{}{}", VersionSpec::starts_with_str, pred.m_version); + } } if constexpr (std::is_same_v) { @@ -283,11 +295,23 @@ namespace mamba::specs return m_tree.evaluate([&point](const auto& node) { return node.contains(point); }); } + auto VersionSpec::is_explicitly_free() const -> bool + { + const auto free_pred = VersionPredicate::make_free(); + const auto is_free_pred = [&free_pred](const auto& node) { return node == free_pred; }; + return m_tree.empty() || ((m_tree.size() == 1) && m_tree.evaluate(is_free_pred)); + } + auto VersionSpec::str() const -> std::string { return fmt::format("{}", *this); } + auto VersionSpec::str_conda_build() const -> std::string + { + return fmt::format("{:b}", *this); + } + namespace { template @@ -464,10 +488,10 @@ namespace mamba::specs auto fmt::formatter::parse(format_parse_context& ctx) -> decltype(ctx.begin()) { - // make sure that range is empty - if (ctx.begin() != ctx.end() && *ctx.begin() != '}') + if (auto it = std::find(ctx.begin(), ctx.end(), 'b'); it < ctx.end()) { - throw fmt::format_error("Invalid format"); + conda_build_form = true; + return ++it; } return ctx.begin(); } @@ -511,7 +535,7 @@ fmt::formatter::format( } if constexpr (std::is_same_v) { - out = fmt::format_to(out, "{}", token); + out = fmt::format_to(out, conda_build_form ? "{:b}" : "{}", token); } } ); diff --git a/libmamba/tests/src/specs/test_match_spec.cpp b/libmamba/tests/src/specs/test_match_spec.cpp index 02a3dc73c..f46cfed07 100644 --- a/libmamba/tests/src/specs/test_match_spec.cpp +++ b/libmamba/tests/src/specs/test_match_spec.cpp @@ -67,29 +67,29 @@ TEST_SUITE("specs::match_spec") { { auto ms = MatchSpec::parse("xtensor==0.12.3"); - CHECK_EQ(ms.version(), "0.12.3"); + CHECK_EQ(ms.version().str(), "==0.12.3"); CHECK_EQ(ms.name(), "xtensor"); } { auto ms = MatchSpec::parse(""); - CHECK_EQ(ms.version(), ""); + CHECK_EQ(ms.version().str(), "=*"); CHECK_EQ(ms.name(), ""); } { auto ms = MatchSpec::parse("ipykernel"); - CHECK_EQ(ms.version(), ""); + CHECK_EQ(ms.version().str(), "=*"); CHECK_EQ(ms.name(), "ipykernel"); } { auto ms = MatchSpec::parse("ipykernel "); - CHECK_EQ(ms.version(), ""); + CHECK_EQ(ms.version().str(), "=*"); CHECK_EQ(ms.name(), "ipykernel"); } { auto ms = MatchSpec::parse("numpy 1.7*"); - CHECK_EQ(ms.version(), "1.7*"); + CHECK_EQ(ms.version().str(), "=1.7"); CHECK_EQ(ms.name(), "numpy"); - CHECK_EQ(ms.conda_build_form(), "numpy 1.7*"); + CHECK_EQ(ms.conda_build_form(), "numpy 1.7.*"); CHECK_EQ(ms.str(), "numpy=1.7"); } { @@ -98,11 +98,11 @@ TEST_SUITE("specs::match_spec") // CHECK_EQ(ms.version, "1.7|1.8"); CHECK_EQ(ms.name(), "numpy"); CHECK_EQ(ms.brackets["version"], "1.7|1.8"); - CHECK_EQ(ms.str(), "numpy[version='1.7|1.8']"); + CHECK_EQ(ms.str(), "numpy[version='==1.7|==1.8']"); } { auto ms = MatchSpec::parse("conda-forge/linux-64::xtensor==0.12.3"); - CHECK_EQ(ms.version(), "0.12.3"); + CHECK_EQ(ms.version().str(), "==0.12.3"); CHECK_EQ(ms.name(), "xtensor"); REQUIRE(ms.channel().has_value()); CHECK_EQ(ms.channel()->location(), "conda-forge"); @@ -111,7 +111,7 @@ TEST_SUITE("specs::match_spec") } { auto ms = MatchSpec::parse("conda-forge::foo[build=3](target=blarg,optional)"); - CHECK_EQ(ms.version(), ""); + CHECK_EQ(ms.version().str(), "=*"); CHECK_EQ(ms.name(), "foo"); REQUIRE(ms.channel().has_value()); CHECK_EQ(ms.channel()->location(), "conda-forge"); @@ -136,7 +136,7 @@ TEST_SUITE("specs::match_spec") "https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2" ); CHECK_EQ(ms.name(), "_libgcc_mutex"); - CHECK_EQ(ms.version(), "0.1"); + CHECK_EQ(ms.version().str(), "==0.1"); CHECK_EQ(ms.build_string(), "conda_forge"); CHECK_EQ( ms.url(), @@ -149,7 +149,7 @@ 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.name(), "libgcc-ng"); - CHECK_EQ(ms.version(), "11.2.0"); + CHECK_EQ(ms.version().str(), "==11.2.0"); CHECK_EQ(ms.build_string(), "h1d223b6_13"); CHECK_EQ( ms.url(), @@ -162,7 +162,7 @@ TEST_SUITE("specs::match_spec") "/home/randomguy/Downloads/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2" ); CHECK_EQ(ms.name(), "_libgcc_mutex"); - CHECK_EQ(ms.version(), "0.1"); + CHECK_EQ(ms.version().str(), "==0.1"); CHECK_EQ(ms.build_string(), "conda_forge"); if (util::on_win) { @@ -199,21 +199,21 @@ TEST_SUITE("specs::match_spec") } { auto ms = MatchSpec::parse("foo=1.0=2"); - CHECK_EQ(ms.conda_build_form(), "foo 1.0 2"); - CHECK_EQ(ms.str(), "foo==1.0=2"); + CHECK_EQ(ms.conda_build_form(), "foo 1.0.* 2"); + CHECK_EQ(ms.str(), "foo=1.0=2"); } { auto ms = MatchSpec::parse("foo=1.0=2[md5=123123123, license=BSD-3, fn='test 123.tar.bz2']" ); - CHECK_EQ(ms.conda_build_form(), "foo 1.0 2"); - CHECK_EQ(ms.str(), "foo==1.0=2[md5=123123123,license=BSD-3,fn='test 123.tar.bz2']"); + CHECK_EQ(ms.conda_build_form(), "foo 1.0.* 2"); + CHECK_EQ(ms.str(), "foo=1.0=2[md5=123123123,license=BSD-3,fn='test 123.tar.bz2']"); } { auto ms = MatchSpec::parse( "foo=1.0=2[md5=123123123, license=BSD-3, fn='test 123.tar.bz2', url='abcdef']" ); - CHECK_EQ(ms.conda_build_form(), "foo 1.0 2"); - CHECK_EQ(ms.str(), "foo==1.0=2[url=abcdef,md5=123123123,license=BSD-3]"); + CHECK_EQ(ms.conda_build_form(), "foo 1.0.* 2"); + CHECK_EQ(ms.str(), "foo=1.0=2[url=abcdef,md5=123123123,license=BSD-3]"); } { auto ms = MatchSpec::parse("libblas=*=*mkl"); @@ -222,12 +222,14 @@ TEST_SUITE("specs::match_spec") } { auto ms = MatchSpec::parse("libblas=0.15*"); - CHECK_EQ(ms.conda_build_form(), "libblas 0.15*"); + CHECK_EQ(ms.conda_build_form(), "libblas 0.15*.*"); } { + // '*' is part of the version, not the glob auto ms = MatchSpec::parse("xtensor =0.15*"); - CHECK_EQ(ms.conda_build_form(), "xtensor 0.15*"); - CHECK_EQ(ms.str(), "xtensor=0.15"); + CHECK_EQ(ms.conda_build_form(), "xtensor 0.15*.*"); + CHECK_EQ(ms.str(), "xtensor=0.15*"); + CHECK_EQ(ms.version().str(), "=0.15*"); } { auto ms = MatchSpec::parse("numpy=1.20"); diff --git a/libmamba/tests/src/specs/test_version_spec.cpp b/libmamba/tests/src/specs/test_version_spec.cpp index a68a7abd2..c1bb437d6 100644 --- a/libmamba/tests/src/specs/test_version_spec.cpp +++ b/libmamba/tests/src/specs/test_version_spec.cpp @@ -80,6 +80,7 @@ TEST_SUITE("specs::version_spec") CHECK_FALSE(sw.contains(v3)); CHECK_FALSE(sw.contains(v4)); CHECK_EQ(sw.str(), "=2.0"); + CHECK_EQ(sw.str_conda_build(), "2.0.*"); const auto nsw = VersionPredicate::make_not_starts_with(v2); CHECK(nsw.contains(v1)); @@ -397,4 +398,42 @@ TEST_SUITE("specs::version_spec") } } } + + TEST_CASE("VersionSpec::str") + { + SUBCASE("2.3") + { + auto vs = VersionSpec::parse("2.3"); + CHECK_EQ(vs.str(), "==2.3"); + CHECK_EQ(vs.str_conda_build(), "==2.3"); + } + + SUBCASE("=2.3,<3.0") + { + auto vs = VersionSpec::parse("=2.3,<3.0"); + CHECK_EQ(vs.str(), "=2.3,<3.0"); + CHECK_EQ(vs.str_conda_build(), "2.3.*,<3.0"); + } + } + + TEST_CASE("VersionSpec::is_free") + { + { + using namespace mamba::util; + + auto parser = InfixParser{}; + parser.push_variable(VersionPredicate::make_free()); + parser.finalize(); + auto spec = VersionSpec(std::move(parser).tree()); + + CHECK(spec.is_explicitly_free()); + } + + CHECK(VersionSpec().is_explicitly_free()); + CHECK(VersionSpec::parse("*").is_explicitly_free()); + CHECK(VersionSpec::parse("").is_explicitly_free()); + + CHECK_FALSE(VersionSpec::parse("==2.3|!=2.3").is_explicitly_free()); + CHECK_FALSE(VersionSpec::parse("=2.3,<3.0").is_explicitly_free()); + } } diff --git a/libmambapy/tests/test_specs.py b/libmambapy/tests/test_specs.py index ab6e80b73..fb81fb809 100644 --- a/libmambapy/tests/test_specs.py +++ b/libmambapy/tests/test_specs.py @@ -666,4 +666,4 @@ def test_MatchSpec(): ms = MatchSpec.parse("conda-forge::python=3.7=*pypy") # str - assert str(ms) == "conda-forge::python==3.7[build=*pypy]" + assert str(ms) == "conda-forge::python=3.7[build=*pypy]"