From 4e477356fd8b9635ed82dfc67473ebcfd1d25ed8 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Fri, 22 Dec 2023 10:50:50 +0100 Subject: [PATCH 1/3] Add flat_bool_expr_tree:infix_for_each --- .../include/mamba/util/flat_binary_tree.hpp | 26 +++++ .../mamba/util/flat_bool_expr_tree.hpp | 98 ++++++++++++++++++- .../src/util/test_flat_bool_expr_tree.cpp | 58 ++++++++++- 3 files changed, 179 insertions(+), 3 deletions(-) diff --git a/libmamba/include/mamba/util/flat_binary_tree.hpp b/libmamba/include/mamba/util/flat_binary_tree.hpp index 85cd1b1f49..f7d1fd38eb 100644 --- a/libmamba/include/mamba/util/flat_binary_tree.hpp +++ b/libmamba/include/mamba/util/flat_binary_tree.hpp @@ -90,6 +90,9 @@ namespace mamba::util [[nodiscard]] auto right(idx_type idx) const -> idx_type; [[nodiscard]] auto root() const -> idx_type; + template + void dfs_raw(Visitor&& visitor, idx_type start) const; + private: node_list m_nodes; @@ -245,5 +248,28 @@ namespace mamba::util { return add_branch_impl(std::move(branch), left_child, right_child); } + + template + template + void flat_binary_tree::dfs_raw(Visitor&& visitor, idx_type start_idx) const + { + if (is_leaf(start_idx)) + { + visitor.on_leaf(*this, start_idx); + } + else + { + const auto left_idx = left(start_idx); + const auto right_idx = right(start_idx); + + visitor.on_branch_left_before(*this, start_idx, left_idx); + dfs_raw(visitor, left_idx); + + visitor.on_branch_infix(*this, start_idx, left_idx, right_idx); + + dfs_raw(visitor, right_idx); + visitor.on_branch_right_after(*this, start_idx, right_idx); + } + } } #endif diff --git a/libmamba/include/mamba/util/flat_bool_expr_tree.hpp b/libmamba/include/mamba/util/flat_bool_expr_tree.hpp index 29db94969f..96fe07579e 100644 --- a/libmamba/include/mamba/util/flat_bool_expr_tree.hpp +++ b/libmamba/include/mamba/util/flat_bool_expr_tree.hpp @@ -9,9 +9,7 @@ #include #include -#include #include -#include #include #include #include @@ -150,6 +148,14 @@ namespace mamba::util using tree_type = flat_binary_tree; using size_type = typename tree_type::size_type; + struct LeftParenthesis + { + }; + + struct RightParenthesis + { + }; + flat_bool_expr_tree() = default; flat_bool_expr_tree(const flat_bool_expr_tree&) = default; flat_bool_expr_tree(flat_bool_expr_tree&&) = default; @@ -169,6 +175,9 @@ namespace mamba::util [[nodiscard]] auto evaluate(UnaryFunc&& var_evaluator = {}, bool empty_val = true) const -> bool; + template + void infix_for_each(UnaryFunc&& func) const; + private: using idx_type = typename tree_type::idx_type; @@ -179,6 +188,42 @@ namespace mamba::util tree_type m_tree = {}; }; + template + constexpr auto operator==( + typename flat_bool_expr_tree::LeftParenthesis, + typename flat_bool_expr_tree::LeftParenthesis + ) -> bool + { + return true; + } + + template + constexpr auto operator!=( + typename flat_bool_expr_tree::LeftParenthesis, + typename flat_bool_expr_tree::LeftParenthesis + ) -> bool + { + return false; + } + + template + constexpr auto operator==( + typename flat_bool_expr_tree::RightParenthesis, + typename flat_bool_expr_tree::RightParenthesis + ) -> bool + { + return true; + } + + template + constexpr auto operator!=( + typename flat_bool_expr_tree::RightParenthesis, + typename flat_bool_expr_tree::RightParenthesis + ) -> bool + { + return false; + } + /************************************* * Implementation of PostfixParser * *************************************/ @@ -551,5 +596,54 @@ namespace mamba::util || evaluate_impl(var_eval, m_tree.right(idx)); } } + + template + template + void flat_bool_expr_tree::infix_for_each(UnaryFunc&& func) const + { + struct TreeVisitor + { + using idx_type = typename tree_type::idx_type; + + void on_leaf(const tree_type& tree, idx_type idx) + { + m_func(tree.leaf(idx)); + } + + void on_branch_left_before(const tree_type& tree, idx_type, idx_type left_idx) + { + if (!tree.is_leaf(left_idx)) + { + m_func(LeftParenthesis{}); + } + } + + void + on_branch_infix(const tree_type& tree, idx_type branch_idx, idx_type left_idx, idx_type right_idx) + { + if (!tree.is_leaf(left_idx)) + { + m_func(RightParenthesis{}); + } + m_func(tree.branch(branch_idx)); + if (!tree.is_leaf(right_idx)) + { + m_func(LeftParenthesis{}); + } + } + + void on_branch_right_after(const tree_type& tree, idx_type, idx_type right_idx) + { + if (!tree.is_leaf(right_idx)) + { + m_func(RightParenthesis{}); + } + } + + UnaryFunc m_func; + } tree_visitor{ std::forward(func) }; + + m_tree.dfs_raw(tree_visitor, m_tree.root()); + } } #endif diff --git a/libmamba/tests/src/util/test_flat_bool_expr_tree.cpp b/libmamba/tests/src/util/test_flat_bool_expr_tree.cpp index b1a99d9e22..11e4c8f827 100644 --- a/libmamba/tests/src/util/test_flat_bool_expr_tree.cpp +++ b/libmamba/tests/src/util/test_flat_bool_expr_tree.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include @@ -466,6 +465,7 @@ TEST_SUITE("util::flat_bool_expr_tree") { const auto reference_eval = [](std::array x) -> bool { return (x[0] || x[1]) && (x[2] && (x[3] || x[4])); }; + // Infix: ((x3 or x4) and x2) and (x0 or x1) // Postfix: x0 x1 or x2 x3 x4 or and and auto parser = PostfixParser{}; parser.push_variable(0); @@ -493,6 +493,7 @@ TEST_SUITE("util::flat_bool_expr_tree") const auto reference_eval = [](std::array x) -> bool { return ((x[0] || x[1]) && (x[2] || x[3] || x[4]) && x[5]) || x[6]; }; auto parser = InfixParser{}; + // Infix: ((x0 or x1) and (x2 or x3 or x4) and x5) or x6 parser.push_left_parenthesis(); parser.push_left_parenthesis(); parser.push_variable(0); @@ -524,4 +525,59 @@ TEST_SUITE("util::flat_bool_expr_tree") CHECK_EQ(tree.evaluate(eval), reference_eval(values)); } } + + TEST_CASE("Infix traversal") + { + auto parser = InfixParser{}; + // Infix: ((x0 or x1) and (x2 or x3 or x4) and x5) or x6 + parser.push_left_parenthesis(); + parser.push_left_parenthesis(); + parser.push_variable(0); + parser.push_operator(BoolOperator::logical_or); + parser.push_variable(1); + parser.push_right_parenthesis(); + parser.push_operator(BoolOperator::logical_and); + parser.push_left_parenthesis(); + parser.push_variable(2); + parser.push_operator(BoolOperator::logical_or); + parser.push_variable(3); + parser.push_operator(BoolOperator::logical_or); + parser.push_variable(4); + parser.push_right_parenthesis(); + parser.push_operator(BoolOperator::logical_and); + parser.push_variable(5); + parser.push_right_parenthesis(); + parser.push_operator(BoolOperator::logical_or); + parser.push_variable(6); + parser.finalize(); + auto tree = flat_bool_expr_tree(std::move(parser).tree()); + + auto result = std::string(); + tree.infix_for_each( + [&](const auto& token) + { + using tree_type = decltype(tree); + using Token = std::decay_t; + if constexpr (std::is_same_v) + { + result += '('; + } + if constexpr (std::is_same_v) + { + result += ')'; + } + if constexpr (std::is_same_v) + { + result += (token == BoolOperator::logical_or) ? " or " : " and "; + } + if constexpr (std::is_same_v) + { + result += 'x'; + result += std::to_string(token); + } + } + ); + // There could be many representations, here is one + CHECK_EQ(result, "((x0 or x1) and ((x2 or (x3 or x4)) and x5)) or x6"); + } } From cd4af890bc20d2cb954d9cac7c5577889a038c7e Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Fri, 22 Dec 2023 12:20:13 +0100 Subject: [PATCH 2/3] Add level to Version repr --- libmamba/include/mamba/specs/version.hpp | 84 +++++------------ libmamba/src/specs/version.cpp | 95 ++++++++++++++++++++ libmamba/tests/src/specs/test_version.cpp | 47 +++++++--- libmambapy/src/libmambapy/bindings/specs.cpp | 8 +- libmambapy/tests/test_specs.py | 1 + 5 files changed, 160 insertions(+), 75 deletions(-) diff --git a/libmamba/include/mamba/specs/version.hpp b/libmamba/include/mamba/specs/version.hpp index 358e1b3ccc..d19d3765ae 100644 --- a/libmamba/include/mamba/specs/version.hpp +++ b/libmamba/include/mamba/specs/version.hpp @@ -7,6 +7,7 @@ #ifndef MAMBA_SPECS_VERSION_HPP #define MAMBA_SPECS_VERSION_HPP +#include #include #include #include @@ -110,8 +111,24 @@ namespace mamba::specs [[nodiscard]] auto version() const noexcept -> const CommonVersion&; [[nodiscard]] auto local() const noexcept -> const CommonVersion&; + /** + * A string representation of the version. + * + * May not always be the same as the parsed string (due to reconstruction) but reparsing + * this string will give the same version. + * ``v == Version::parse(v.str())``. + */ [[nodiscard]] auto str() const -> std::string; + /** + * A string truncated of extended representation of the version. + * + * Represent the string with the desired number of parts. + * If the actual number of parts is larger, then the string is truncated. + * If the actual number of parts is smalle, then the string is expanded with zeros. + */ + [[nodiscard]] auto str(std::size_t level) const -> std::string; + [[nodiscard]] auto operator==(const Version& other) const -> bool; [[nodiscard]] auto operator!=(const Version& other) const -> bool; [[nodiscard]] auto operator<(const Version& other) const -> bool; @@ -156,73 +173,20 @@ namespace mamba::specs template <> struct fmt::formatter { - constexpr auto 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 parse(format_parse_context& ctx) -> decltype(ctx.begin()); - template - auto format(const ::mamba::specs::VersionPartAtom atom, FormatContext& ctx) - { - return fmt::format_to(ctx.out(), "{}{}", atom.numeral(), atom.literal()); - } + auto format(const ::mamba::specs::VersionPartAtom atom, format_context& ctx) + -> decltype(ctx.out()); }; template <> struct fmt::formatter { - constexpr auto 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(); - } + std::optional m_level; - template - auto format(const ::mamba::specs::Version v, FormatContext& ctx) - { - auto out = ctx.out(); - if (v.epoch() != 0) - { - out = fmt::format_to(ctx.out(), "{}!", v.epoch()); - } - - auto format_version_to = [](auto l_out, const auto& version) - { - bool first = true; - for (const auto& part : version) - { - if (first) - { - first = false; - } - else - { - l_out = fmt::format_to(l_out, "."); - } - for (const auto& atom : part) - { - l_out = fmt::format_to(l_out, "{}", atom); - } - } - return l_out; - }; - out = format_version_to(out, v.version()); - if (!v.local().empty()) - { - out = fmt::format_to(out, "+"); - out = format_version_to(out, v.local()); - } - return out; - } + auto parse(format_parse_context& ctx) -> decltype(ctx.begin()); + + auto format(const ::mamba::specs::Version v, format_context& ctx) -> decltype(ctx.out()); }; #endif diff --git a/libmamba/src/specs/version.cpp b/libmamba/src/specs/version.cpp index 6221fcfe9a..6578cfd405 100644 --- a/libmamba/src/specs/version.cpp +++ b/libmamba/src/specs/version.cpp @@ -174,6 +174,31 @@ namespace mamba::specs { return compare_three_way(*this, other) != strong_ordering::less; } +} + +auto +fmt::formatter::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::format( + const ::mamba::specs::VersionPartAtom atom, + format_context& ctx +) -> decltype(ctx.out()) +{ + return fmt::format_to(ctx.out(), "{}{}", atom.numeral(), atom.literal()); +} + +namespace mamba::specs +{ /******************************* * Implementation of Version * @@ -206,6 +231,15 @@ namespace mamba::specs return fmt::format("{}", *this); } + auto Version::str(std::size_t level) const -> std::string + { + // We should be able to do, as it works with numbers but it is not clear how this works + // with the cusotm parser + // return fmt::format("{:{}}", *this, level); + auto fmt = fmt::format("{{:{}}}", level); + return fmt::format(fmt, *this); + } + namespace { /** @@ -713,3 +747,64 @@ namespace mamba::specs } } } + +auto +fmt::formatter::parse(format_parse_context& ctx) -> decltype(ctx.begin()) +{ + // make sure that range is not empty + if (ctx.begin() == ctx.end() || *ctx.begin() == '}') + { + return ctx.begin(); + } + std::size_t val = 0; + auto [ptr, ec] = std::from_chars(ctx.begin(), ctx.end(), val); + if (ec != std::errc()) + { + throw fmt::format_error("Invalid format" + std::string(ctx.begin(), ctx.end())); + } + m_level = val; + return ptr; +} + +auto +fmt::formatter::format(const ::mamba::specs::Version v, format_context& ctx) + -> decltype(ctx.out()) +{ + auto out = ctx.out(); + if (v.epoch() != 0) + { + out = fmt::format_to(ctx.out(), "{}!", v.epoch()); + } + + + auto format_version_to = [this](auto l_out, const auto& version) + { + auto const n_levels = m_level.value_or(version.size()); + for (std::size_t i = 0; i < n_levels; ++i) + { + if (i != 0) + { + l_out = fmt::format_to(l_out, "."); + } + if (i < version.size()) + { + for (const auto& atom : version[i]) + { + l_out = fmt::format_to(l_out, "{}", atom); + } + } + else + { + l_out = fmt::format_to(l_out, "0"); + } + } + return l_out; + }; + out = format_version_to(out, v.version()); + if (!v.local().empty()) + { + out = fmt::format_to(out, "+"); + out = format_version_to(out, v.local()); + } + return out; +} diff --git a/libmamba/tests/src/specs/test_version.cpp b/libmamba/tests/src/specs/test_version.cpp index f42d956886..752fa952d6 100644 --- a/libmamba/tests/src/specs/test_version.cpp +++ b/libmamba/tests/src/specs/test_version.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include @@ -306,20 +305,40 @@ TEST_SUITE("specs::version") TEST_CASE("version_format") { - // clang-format off - CHECK_EQ( - Version(0, {{{11, "a"}, {0, "post"}}, {{3}}, {{4, "dev"}}}).str(), - "11a0post.3.4dev" - ); - CHECK_EQ( - Version(1, {{{11, "a"}, {0}}, {{3}}, {{4, "dev"}}}).str(), - "1!11a0.3.4dev" - ); - CHECK_EQ( - Version(1, {{{11, "a"}, {0}}, {{3}}, {{4, "dev"}}}, {{{1}}, {{2}}}).str(), - "1!11a0.3.4dev+1.2" + SUBCASE("11a0post.3.4dev") + { + auto v = Version(0, { { { 11, "a" }, { 0, "post" } }, { { 3 } }, { { 4, "dev" } } }); + CHECK_EQ(v.str(), "11a0post.3.4dev"); + CHECK_EQ(v.str(1), "11a0post"); + CHECK_EQ(v.str(2), "11a0post.3"); + CHECK_EQ(v.str(3), "11a0post.3.4dev"); + CHECK_EQ(v.str(4), "11a0post.3.4dev.0"); + CHECK_EQ(v.str(5), "11a0post.3.4dev.0.0"); + } + + SUBCASE("1!11a0.3.4dev") + { + auto v = Version(1, { { { 11, "a" }, { 0 } }, { { 3 } }, { { 4, "dev" } } }); + CHECK_EQ(v.str(), "1!11a0.3.4dev"); + CHECK_EQ(v.str(1), "1!11a0"); + CHECK_EQ(v.str(2), "1!11a0.3"); + CHECK_EQ(v.str(3), "1!11a0.3.4dev"); + CHECK_EQ(v.str(4), "1!11a0.3.4dev.0"); + } + + SUBCASE("1!11a0.3.4dev+1.2") + { + auto v = Version( + 1, + { { { 11, "a" }, { 0 } }, { { 3 } }, { { 4, "dev" } } }, + { { { 1 } }, { { 2 } } } ); - // clang-format on + CHECK_EQ(v.str(), "1!11a0.3.4dev+1.2"); + CHECK_EQ(v.str(1), "1!11a0+1"); + CHECK_EQ(v.str(2), "1!11a0.3+1.2"); + CHECK_EQ(v.str(3), "1!11a0.3.4dev+1.2.0"); + CHECK_EQ(v.str(4), "1!11a0.3.4dev.0+1.2.0.0"); + } } /** diff --git a/libmambapy/src/libmambapy/bindings/specs.cpp b/libmambapy/src/libmambapy/bindings/specs.cpp index 7336dfe8f5..681f361869 100644 --- a/libmambapy/src/libmambapy/bindings/specs.cpp +++ b/libmambapy/src/libmambapy/bindings/specs.cpp @@ -525,7 +525,13 @@ namespace mambapy .def_property_readonly("local", &Version::local) .def("starts_with", &Version::starts_with, py::arg("prefix")) .def("compatible_with", &Version::compatible_with, py::arg("older"), py::arg("level")) - .def("__str__", &Version::str) + .def("__str__", [](const Version& v) { return v.str(); }) + .def("str", [](const Version& v) { return v.str(); }) + .def( + "str", + [](const Version& v, std::size_t level) { return v.str(level); }, + py::arg("level") + ) .def(py::self == py::self) .def(py::self != py::self) .def(py::self < py::self) diff --git a/libmambapy/tests/test_specs.py b/libmambapy/tests/test_specs.py index 6af55330ef..cefaa327e3 100644 --- a/libmambapy/tests/test_specs.py +++ b/libmambapy/tests/test_specs.py @@ -604,6 +604,7 @@ def test_Version(): # str assert str(v) == "3!1.3ab2.4+42.0alpha" + assert v.str(level=1) == "3!1+42" # Copy assert copy.deepcopy(v) == v From 49a71377787a7cb11a56207b43a722916c9ba4d7 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Fri, 22 Dec 2023 13:10:09 +0100 Subject: [PATCH 3/3] Add VersionSpec::str --- libmamba/include/mamba/specs/version_spec.hpp | 50 ++++++ libmamba/src/specs/version_spec.cpp | 170 +++++++++++++++++- .../tests/src/specs/test_version_spec.cpp | 15 ++ libmambapy/src/libmambapy/bindings/specs.cpp | 1 + libmambapy/tests/test_specs.py | 7 +- 5 files changed, 233 insertions(+), 10 deletions(-) diff --git a/libmamba/include/mamba/specs/version_spec.hpp b/libmamba/include/mamba/specs/version_spec.hpp index 1a08293de6..e0d8bd325e 100644 --- a/libmamba/include/mamba/specs/version_spec.hpp +++ b/libmamba/include/mamba/specs/version_spec.hpp @@ -11,11 +11,16 @@ #include #include +#include + #include "mamba/specs/version.hpp" #include "mamba/util/flat_bool_expr_tree.hpp" namespace mamba::specs { + /** + * A stateful unary boolean function on the Version space. + */ class VersionPredicate { public: @@ -35,8 +40,13 @@ namespace mamba::specs /** Construct an free interval. */ VersionPredicate() = default; + /** + * True if the predicate contains the given version. + */ [[nodiscard]] auto contains(const Version& point) const -> bool; + [[nodiscard]] auto str() const -> std::string; + private: struct free_interval @@ -90,11 +100,23 @@ namespace mamba::specs friend auto operator==(not_starts_with, not_starts_with) -> bool; friend auto operator==(compatible_with, compatible_with) -> bool; friend auto operator==(const VersionPredicate& lhs, const VersionPredicate& rhs) -> bool; + friend class ::fmt::formatter; }; auto operator==(const VersionPredicate& lhs, const VersionPredicate& rhs) -> bool; auto operator!=(const VersionPredicate& lhs, const VersionPredicate& rhs) -> bool; + /** + * Represent a set of versions. + * + * Internally, a VersionSpec is a binary expression tree of union (or) or intersections (and) + * of the sets represented by VersionPredicate. + * + * The VersionSpec can itself be considered a complex predicate on the space of Version. + * + * Due to the complex nature of the expression system (comutativity, associativity, etc.), there + * is no easy way to say if two VersionSpecs are equal. + */ class VersionSpec { public: @@ -123,12 +145,24 @@ namespace mamba::specs VersionSpec() = default; explicit VersionSpec(tree_type&& tree) noexcept; + /** + * A string representation of the version spec. + * + * May not always be the same as the parsed string (due to reconstruction) but reparsing + * this string will give the same version spec. + */ [[nodiscard]] auto str() const -> std::string; + + /** + * True if the set described by the VersionSpec contains the given version. + */ [[nodiscard]] auto contains(const Version& point) const -> bool; private: tree_type m_tree; + + friend class ::fmt::formatter; }; namespace version_spec_literals @@ -137,4 +171,20 @@ namespace mamba::specs } } +template <> +struct fmt::formatter +{ + auto parse(format_parse_context& ctx) -> decltype(ctx.begin()); + + auto format(const ::mamba::specs::VersionPredicate& pred, format_context& ctx) + -> decltype(ctx.out()); +}; + +template <> +struct fmt::formatter +{ + auto parse(format_parse_context& ctx) -> decltype(ctx.begin()); + + auto format(const ::mamba::specs::VersionSpec& spec, format_context& ctx) -> decltype(ctx.out()); +}; #endif diff --git a/libmamba/src/specs/version_spec.cpp b/libmamba/src/specs/version_spec.cpp index 8940a1b5b7..addd8d5b53 100644 --- a/libmamba/src/specs/version_spec.cpp +++ b/libmamba/src/specs/version_spec.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include @@ -156,6 +155,11 @@ namespace mamba::specs return VersionPredicate(std::move(ver), compatible_with{ level }); } + auto VersionPredicate::str() const -> std::string + { + return fmt::format("{}", *this); + } + VersionPredicate::VersionPredicate(Version ver, BinaryOperator op) : m_version(std::move(ver)) , m_operator(std::move(op)) @@ -171,6 +175,99 @@ namespace mamba::specs { return !(lhs == rhs); } +} + +auto +fmt::formatter::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::format( + const ::mamba::specs::VersionPredicate& pred, + format_context& ctx +) -> decltype(ctx.out()) +{ + using VersionPredicate = typename mamba::specs::VersionPredicate; + using VersionSpec = typename mamba::specs::VersionSpec; + using Version = typename mamba::specs::Version; + + auto out = ctx.out(); + std::visit( + [&](const auto& op) + { + using Op = std::decay_t; + if constexpr (std::is_same_v) + { + out = fmt::format_to( + out, + "{}{}", + VersionSpec::starts_with_str, + VersionSpec::glob_suffix_token + ); + } + if constexpr (std::is_same_v>) + { + out = fmt::format_to(out, "{}{}", VersionSpec::equal_str, pred.m_version); + } + if constexpr (std::is_same_v>) + { + out = fmt::format_to(out, "{}{}", VersionSpec::not_equal_str, pred.m_version); + } + if constexpr (std::is_same_v>) + { + out = fmt::format_to(out, "{}{}", VersionSpec::greater_str, pred.m_version); + } + if constexpr (std::is_same_v>) + { + out = fmt::format_to(out, "{}{}", VersionSpec::greater_equal_str, pred.m_version); + } + if constexpr (std::is_same_v>) + { + out = fmt::format_to(out, "{}{}", VersionSpec::less_str, pred.m_version); + } + if constexpr (std::is_same_v>) + { + out = fmt::format_to(out, "{}{}", VersionSpec::less_equal_str, pred.m_version); + } + if constexpr (std::is_same_v) + { + out = fmt::format_to(out, "{}{}", VersionSpec::starts_with_str, pred.m_version); + } + if constexpr (std::is_same_v) + { + out = fmt::format_to( + out, + "{}{}{}", + VersionSpec::not_equal_str, + pred.m_version, + VersionSpec::glob_suffix_str + ); + } + if constexpr (std::is_same_v) + { + out = fmt::format_to( + out, + "{}{}", + VersionSpec::compatible_str, + pred.m_version.str(op.level) + ); + } + }, + pred.m_operator + ); + return out; +} + +namespace mamba::specs +{ /******************************** * VersionSpec Implementation * @@ -186,6 +283,11 @@ namespace mamba::specs return m_tree.evaluate([&point](const auto& node) { return node.contains(point); }); } + auto VersionSpec::str() const -> std::string + { + return fmt::format("{}", *this); + } + namespace { auto is_char(std::string_view str, char c) -> bool @@ -229,15 +331,15 @@ namespace mamba::specs { auto ver = Version::parse(str.substr(VersionSpec::compatible_str.size())); // in ``~=1.1`` level is assumed to be 1, in ``~=1.1.1`` level 2, etc. - static std::size_t constexpr one = std::size_t(1); // MSVC + static constexpr std::size_t one = std::size_t(1); // MSVC const std::size_t level = std::max(ver.version().size(), one) - one; return VersionPredicate::make_compatible_with(std::move(ver), level); } const bool has_glob_suffix = util::ends_with(str, VersionSpec::glob_suffix_str); - std::size_t const glob_len = has_glob_suffix * VersionSpec::glob_suffix_str.size(); + const std::size_t glob_len = has_glob_suffix * VersionSpec::glob_suffix_str.size(); if (util::starts_with(str, VersionSpec::equal_str)) { - std::size_t const start = VersionSpec::equal_str.size(); + const std::size_t start = VersionSpec::equal_str.size(); // Glob suffix changes meaning for ==1.3.* if (has_glob_suffix) { @@ -252,7 +354,7 @@ namespace mamba::specs } if (util::starts_with(str, VersionSpec::not_equal_str)) { - std::size_t const start = VersionSpec::not_equal_str.size(); + const std::size_t start = VersionSpec::not_equal_str.size(); // Glob suffix changes meaning for !=1.3.* if (has_glob_suffix) { @@ -267,7 +369,7 @@ namespace mamba::specs } if (util::starts_with(str, VersionSpec::starts_with_str)) { - std::size_t const start = VersionSpec::starts_with_str.size(); + const std::size_t start = VersionSpec::starts_with_str.size(); // Glob suffix does not change meaning for =1.3.* return VersionPredicate::make_starts_with( Version::parse(str.substr(start, str.size() - glob_len - start)) @@ -279,8 +381,8 @@ namespace mamba::specs if (util::ends_with(str, VersionSpec::glob_suffix_token)) { // either ".*" or "*" - static std::size_t constexpr one = std::size_t(1); // MSVC - std::size_t const len = str.size() - std::max(glob_len, one); + static constexpr std::size_t one = std::size_t(1); // MSVC + const std::size_t len = str.size() - std::max(glob_len, one); return VersionPredicate::make_starts_with(Version::parse(str.substr(0, len))); } else @@ -348,3 +450,55 @@ 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() != '}') + { + throw fmt::format_error("Invalid format"); + } + return ctx.begin(); +} + +auto +fmt::formatter::format( + const ::mamba::specs::VersionSpec& spec, + format_context& ctx +) -> decltype(ctx.out()) +{ + auto out = ctx.out(); + spec.m_tree.infix_for_each( + [&](const auto& token) + { + using VersionSpec = typename mamba::specs::VersionSpec; + using tree_type = typename VersionSpec::tree_type; + using Token = std::decay_t; + if constexpr (std::is_same_v) + { + out = fmt::format_to(out, "{}", VersionSpec::left_parenthesis_token); + } + if constexpr (std::is_same_v) + { + out = fmt::format_to(out, "{}", VersionSpec::right_parenthesis_token); + } + if constexpr (std::is_same_v) + { + if (token == tree_type::operator_type::logical_or) + { + out = fmt::format_to(out, "{}", VersionSpec::or_token); + } + else + { + out = fmt::format_to(out, "{}", VersionSpec::and_token); + } + } + if constexpr (std::is_same_v) + { + out = fmt::format_to(out, "{}", token); + } + } + ); + return out; +} diff --git a/libmamba/tests/src/specs/test_version_spec.cpp b/libmamba/tests/src/specs/test_version_spec.cpp index 9d4443f5ff..5e536a35cd 100644 --- a/libmamba/tests/src/specs/test_version_spec.cpp +++ b/libmamba/tests/src/specs/test_version_spec.cpp @@ -29,42 +29,49 @@ TEST_SUITE("specs::version_spec") CHECK(free.contains(v2)); CHECK(free.contains(v3)); CHECK(free.contains(v4)); + CHECK_EQ(free.str(), "=*"); const auto eq = VersionPredicate::make_equal_to(v2); CHECK_FALSE(eq.contains(v1)); CHECK(eq.contains(v2)); CHECK_FALSE(eq.contains(v3)); CHECK_FALSE(eq.contains(v4)); + CHECK_EQ(eq.str(), "==2.0"); const auto ne = VersionPredicate::make_not_equal_to(v2); CHECK(ne.contains(v1)); CHECK_FALSE(ne.contains(v2)); CHECK(ne.contains(v3)); CHECK(ne.contains(v4)); + CHECK_EQ(ne.str(), "!=2.0"); const auto gt = VersionPredicate::make_greater(v2); CHECK_FALSE(gt.contains(v1)); CHECK_FALSE(gt.contains(v2)); CHECK(gt.contains(v3)); CHECK(gt.contains(v4)); + CHECK_EQ(gt.str(), ">2.0"); const auto ge = VersionPredicate::make_greater_equal(v2); CHECK_FALSE(ge.contains(v1)); CHECK(ge.contains(v2)); CHECK(ge.contains(v3)); CHECK(ge.contains(v4)); + CHECK_EQ(ge.str(), ">=2.0"); const auto lt = VersionPredicate::make_less(v2); CHECK(lt.contains(v1)); CHECK_FALSE(lt.contains(v2)); CHECK_FALSE(lt.contains(v3)); CHECK_FALSE(lt.contains(v4)); + CHECK_EQ(lt.str(), "<2.0"); const auto le = VersionPredicate::make_less_equal(v2); CHECK(le.contains(v1)); CHECK(le.contains(v2)); CHECK_FALSE(le.contains(v3)); CHECK_FALSE(le.contains(v4)); + CHECK_EQ(le.str(), "<=2.0"); const auto sw = VersionPredicate::make_starts_with(v2); CHECK_FALSE(sw.contains(v1)); @@ -72,6 +79,7 @@ TEST_SUITE("specs::version_spec") CHECK(sw.contains(v201)); CHECK_FALSE(sw.contains(v3)); CHECK_FALSE(sw.contains(v4)); + CHECK_EQ(sw.str(), "=2.0"); const auto nsw = VersionPredicate::make_not_starts_with(v2); CHECK(nsw.contains(v1)); @@ -79,6 +87,7 @@ TEST_SUITE("specs::version_spec") CHECK_FALSE(nsw.contains(v201)); CHECK(nsw.contains(v3)); CHECK(nsw.contains(v4)); + CHECK_EQ(nsw.str(), "!=2.0.*"); const auto cp2 = VersionPredicate::make_compatible_with(v2, 2); CHECK_FALSE(cp2.contains(v1)); @@ -86,6 +95,7 @@ TEST_SUITE("specs::version_spec") CHECK(cp2.contains(v201)); CHECK_FALSE(cp2.contains(v3)); CHECK_FALSE(cp2.contains(v4)); + CHECK_EQ(cp2.str(), "~=2.0"); const auto cp3 = VersionPredicate::make_compatible_with(v2, 3); CHECK_FALSE(cp3.contains(v1)); @@ -93,6 +103,7 @@ TEST_SUITE("specs::version_spec") CHECK_FALSE(cp3.contains(v201)); CHECK_FALSE(cp3.contains(v3)); CHECK_FALSE(cp3.contains(v4)); + CHECK_EQ(cp3.str(), "~=2.0.0"); const auto predicates = std::array{ free, eq, ne, lt, le, gt, ge, sw, cp2, cp3 }; for (std::size_t i = 0; i < predicates.size(); ++i) @@ -140,6 +151,10 @@ TEST_SUITE("specs::version_spec") CHECK_FALSE(spec.contains(Version(0, { { { 2 } }, { { 0 } }, { { 0 } } }))); // 2.0.0 CHECK_FALSE(spec.contains(Version(0, { { { 2 } }, { { 1 } } }))); // 2.1 CHECK_FALSE(spec.contains(Version(0, { { { 2 } }, { { 3 } } }))); // 2.3 + + // Note this won't always be the same as the parsed string because of the tree + // serialization + CHECK_EQ(spec.str(), "<2.0|(>2.3,<=2.8.0)"); } } diff --git a/libmambapy/src/libmambapy/bindings/specs.cpp b/libmambapy/src/libmambapy/bindings/specs.cpp index 681f361869..43a483475a 100644 --- a/libmambapy/src/libmambapy/bindings/specs.cpp +++ b/libmambapy/src/libmambapy/bindings/specs.cpp @@ -561,6 +561,7 @@ namespace mambapy .def_readonly_static("glob_suffix_token", &VersionSpec::glob_suffix_token) .def_static("parse", &VersionSpec::parse, py::arg("str")) .def("contains", &VersionSpec::contains, py::arg("point")) + .def("__str__", &VersionSpec::str) .def("__copy__", ©) .def("__deepcopy__", &deepcopy, py::arg("memo")); } diff --git a/libmambapy/tests/test_specs.py b/libmambapy/tests/test_specs.py index cefaa327e3..4438c42cd1 100644 --- a/libmambapy/tests/test_specs.py +++ b/libmambapy/tests/test_specs.py @@ -651,5 +651,8 @@ def test_VersionSpec(): assert not vs.contains(Version.parse("1.1")) assert vs.contains(Version.parse("2.1")) - # Copy - copy.deepcopy(vs) # No easy comaprison + # str + assert str(vs) == ">2.0,<3.0" + + # Copy, no easy comparison, this may not work for all specs + assert str(copy.deepcopy(vs)) == str(vs)