Add VersionSpec::str (#3081)

* Add flat_bool_expr_tree:infix_for_each

* Add level to Version repr

* Add VersionSpec::str
This commit is contained in:
Antoine Prouvost 2023-12-22 14:19:33 +01:00 committed by GitHub
parent 2d85759c13
commit 152857f059
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 571 additions and 87 deletions

View File

@ -7,6 +7,7 @@
#ifndef MAMBA_SPECS_VERSION_HPP
#define MAMBA_SPECS_VERSION_HPP
#include <optional>
#include <string>
#include <string_view>
#include <vector>
@ -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<mamba::specs::VersionPartAtom>
{
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 <class FormatContext>
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<mamba::specs::Version>
{
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<std::size_t> m_level;
template <class FormatContext>
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 parse(format_parse_context& ctx) -> decltype(ctx.begin());
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 format(const ::mamba::specs::Version v, format_context& ctx) -> decltype(ctx.out());
};
#endif

View File

@ -11,11 +11,16 @@
#include <string_view>
#include <variant>
#include <fmt/format.h>
#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<VersionPredicate>;
};
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<VersionSpec>;
};
namespace version_spec_literals
@ -137,4 +171,20 @@ namespace mamba::specs
}
}
template <>
struct fmt::formatter<mamba::specs::VersionPredicate>
{
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<mamba::specs::VersionSpec>
{
auto parse(format_parse_context& ctx) -> decltype(ctx.begin());
auto format(const ::mamba::specs::VersionSpec& spec, format_context& ctx) -> decltype(ctx.out());
};
#endif

View File

@ -90,6 +90,9 @@ namespace mamba::util
[[nodiscard]] auto right(idx_type idx) const -> idx_type;
[[nodiscard]] auto root() const -> idx_type;
template <typename Visitor>
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 <typename B, typename L>
template <typename Visitor>
void flat_binary_tree<B, L>::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

View File

@ -9,9 +9,7 @@
#include <cassert>
#include <functional>
#include <iterator>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
@ -150,6 +148,14 @@ namespace mamba::util
using tree_type = flat_binary_tree<operator_type, variable_type>;
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 <typename UnaryFunc>
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 <typename V>
constexpr auto operator==(
typename flat_bool_expr_tree<V>::LeftParenthesis,
typename flat_bool_expr_tree<V>::LeftParenthesis
) -> bool
{
return true;
}
template <typename V>
constexpr auto operator!=(
typename flat_bool_expr_tree<V>::LeftParenthesis,
typename flat_bool_expr_tree<V>::LeftParenthesis
) -> bool
{
return false;
}
template <typename V>
constexpr auto operator==(
typename flat_bool_expr_tree<V>::RightParenthesis,
typename flat_bool_expr_tree<V>::RightParenthesis
) -> bool
{
return true;
}
template <typename V>
constexpr auto operator!=(
typename flat_bool_expr_tree<V>::RightParenthesis,
typename flat_bool_expr_tree<V>::RightParenthesis
) -> bool
{
return false;
}
/*************************************
* Implementation of PostfixParser *
*************************************/
@ -551,5 +596,54 @@ namespace mamba::util
|| evaluate_impl(var_eval, m_tree.right(idx));
}
}
template <typename V>
template <typename UnaryFunc>
void flat_bool_expr_tree<V>::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<UnaryFunc>(func) };
m_tree.dfs_raw(tree_visitor, m_tree.root());
}
}
#endif

View File

@ -174,6 +174,31 @@ namespace mamba::specs
{
return compare_three_way(*this, other) != strong_ordering::less;
}
}
auto
fmt::formatter<mamba::specs::VersionPartAtom>::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::VersionPartAtom>::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<mamba::specs::Version>::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<mamba::specs::Version>::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;
}

View File

@ -6,7 +6,6 @@
#include <algorithm>
#include <array>
#include <cassert>
#include <stdexcept>
#include <type_traits>
@ -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<mamba::specs::VersionPredicate>::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::VersionPredicate>::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<decltype(op)>;
if constexpr (std::is_same_v<Op, VersionPredicate::free_interval>)
{
out = fmt::format_to(
out,
"{}{}",
VersionSpec::starts_with_str,
VersionSpec::glob_suffix_token
);
}
if constexpr (std::is_same_v<Op, std::equal_to<Version>>)
{
out = fmt::format_to(out, "{}{}", VersionSpec::equal_str, pred.m_version);
}
if constexpr (std::is_same_v<Op, std::not_equal_to<Version>>)
{
out = fmt::format_to(out, "{}{}", VersionSpec::not_equal_str, pred.m_version);
}
if constexpr (std::is_same_v<Op, std::greater<Version>>)
{
out = fmt::format_to(out, "{}{}", VersionSpec::greater_str, pred.m_version);
}
if constexpr (std::is_same_v<Op, std::greater_equal<Version>>)
{
out = fmt::format_to(out, "{}{}", VersionSpec::greater_equal_str, pred.m_version);
}
if constexpr (std::is_same_v<Op, std::less<Version>>)
{
out = fmt::format_to(out, "{}{}", VersionSpec::less_str, pred.m_version);
}
if constexpr (std::is_same_v<Op, std::less_equal<Version>>)
{
out = fmt::format_to(out, "{}{}", VersionSpec::less_equal_str, pred.m_version);
}
if constexpr (std::is_same_v<Op, VersionPredicate::starts_with>)
{
out = fmt::format_to(out, "{}{}", VersionSpec::starts_with_str, pred.m_version);
}
if constexpr (std::is_same_v<Op, VersionPredicate::not_starts_with>)
{
out = fmt::format_to(
out,
"{}{}{}",
VersionSpec::not_equal_str,
pred.m_version,
VersionSpec::glob_suffix_str
);
}
if constexpr (std::is_same_v<Op, VersionPredicate::compatible_with>)
{
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<mamba::specs::VersionSpec>::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::VersionSpec>::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<decltype(token)>;
if constexpr (std::is_same_v<Token, tree_type::LeftParenthesis>)
{
out = fmt::format_to(out, "{}", VersionSpec::left_parenthesis_token);
}
if constexpr (std::is_same_v<Token, tree_type::RightParenthesis>)
{
out = fmt::format_to(out, "{}", VersionSpec::right_parenthesis_token);
}
if constexpr (std::is_same_v<Token, tree_type::operator_type>)
{
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<Token, tree_type::variable_type>)
{
out = fmt::format_to(out, "{}", token);
}
}
);
return out;
}

View File

@ -6,7 +6,6 @@
#include <algorithm>
#include <array>
#include <string>
#include <vector>
#include <doctest/doctest.h>
@ -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"
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 } } }
);
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"
);
// 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");
}
}
/**

View File

@ -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)");
}
}

View File

@ -8,7 +8,6 @@
#include <array>
#include <stdexcept>
#include <string>
#include <variant>
#include <vector>
#include <doctest/doctest.h>
@ -466,6 +465,7 @@ TEST_SUITE("util::flat_bool_expr_tree")
{
const auto reference_eval = [](std::array<bool, 5> 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<std::size_t, BoolOperator>{};
parser.push_variable(0);
@ -493,6 +493,7 @@ TEST_SUITE("util::flat_bool_expr_tree")
const auto reference_eval = [](std::array<bool, 7> x) -> bool
{ return ((x[0] || x[1]) && (x[2] || x[3] || x[4]) && x[5]) || x[6]; };
auto parser = InfixParser<std::size_t, BoolOperator>{};
// 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<std::size_t, BoolOperator>{};
// 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<decltype(token)>;
if constexpr (std::is_same_v<Token, tree_type::LeftParenthesis>)
{
result += '(';
}
if constexpr (std::is_same_v<Token, tree_type::RightParenthesis>)
{
result += ')';
}
if constexpr (std::is_same_v<Token, BoolOperator>)
{
result += (token == BoolOperator::logical_or) ? " or " : " and ";
}
if constexpr (std::is_same_v<Token, tree_type::variable_type>)
{
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");
}
}

View File

@ -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)
@ -555,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__", &copy<VersionSpec>)
.def("__deepcopy__", &deepcopy<VersionSpec>, py::arg("memo"));
}

View File

@ -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
@ -650,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)