Remove implicit zero in Version formatting (#3915)

This commit is contained in:
Antoine Prouvost 2025-05-05 16:39:55 +02:00 committed by GitHub
parent e1f0543810
commit 6ad6a6afc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 464 additions and 250 deletions

View File

@ -43,13 +43,6 @@ namespace mamba::specs
[[nodiscard]] auto str() const -> std::string;
auto operator==(const VersionPartAtom& other) const -> bool;
auto operator!=(const VersionPartAtom& other) const -> bool;
auto operator<(const VersionPartAtom& other) const -> bool;
auto operator<=(const VersionPartAtom& other) const -> bool;
auto operator>(const VersionPartAtom& other) const -> bool;
auto operator>=(const VersionPartAtom& other) const -> bool;
private:
// Stored in decreasing size order for performance
@ -57,18 +50,56 @@ namespace mamba::specs
std::size_t m_numeral = 0;
};
auto operator==(const VersionPartAtom& left, const VersionPartAtom& right) -> bool;
auto operator!=(const VersionPartAtom& left, const VersionPartAtom& right) -> bool;
auto operator<(const VersionPartAtom& left, const VersionPartAtom& right) -> bool;
auto operator<=(const VersionPartAtom& left, const VersionPartAtom& right) -> bool;
auto operator>(const VersionPartAtom& left, const VersionPartAtom& right) -> bool;
auto operator>=(const VersionPartAtom& left, const VersionPartAtom& right) -> bool;
extern template VersionPartAtom::VersionPartAtom(std::size_t, std::string);
/**
* A sequence of VersionPartAtom meant to represent a part of a version (e.g. major, minor).
*
* In a version like ``1.3.0post1dev``, the parts are ``1``, ``3``, and ``0post1dev``.
* Version parts can have a arbitrary number of atoms, such as {0, "post"} {1, "dev"}
* in 0post1dev
* in ``0post1dev``.
*
* @see Version::parse for how this is computed from strings.
* @todo Use a small vector of expected size 1 if performance ar not good enough.
*/
using VersionPart = std::vector<VersionPartAtom>;
struct VersionPart
{
/** The atoms of the version part */
std::vector<VersionPartAtom> atoms = {};
/**
* Whether a potential leading zero in the first atom should be considered implicit.
*
* During parsing of ``Version``, if a part starts with a literal atom, it is considered
* the same as if it started with a leading ``0``.
* For instance ``0post1dev`` is parsed in the same way as ``post1dev``.
* Marking it as implicit enables the possibility to remove it when reconstructing a string
* representation.
* This is desirable for compatibility with other version formats, such as Python, where
* a version modifier might be expressed as ``1.3.0.dev3``.
*/
bool implicit_leading_zero = false;
VersionPart();
VersionPart(std::initializer_list<VersionPartAtom> init);
VersionPart(std::vector<VersionPartAtom> atoms, bool implicit_leading_zero);
[[nodiscard]] auto str() const -> std::string;
};
auto operator==(const VersionPart& left, const VersionPart& other) -> bool;
auto operator!=(const VersionPart& left, const VersionPart& other) -> bool;
auto operator<(const VersionPart& left, const VersionPart& other) -> bool;
auto operator<=(const VersionPart& left, const VersionPart& other) -> bool;
auto operator>(const VersionPart& left, const VersionPart& other) -> bool;
auto operator>=(const VersionPart& left, const VersionPart& other) -> bool;
/**
* A sequence of VersionPart meant to represent all parts of a version.
@ -139,13 +170,6 @@ namespace mamba::specs
*/
[[nodiscard]] auto str_glob() 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;
[[nodiscard]] auto operator<=(const Version& other) const -> bool;
[[nodiscard]] auto operator>(const Version& other) const -> bool;
[[nodiscard]] auto operator>=(const Version& other) const -> bool;
/**
* Return true if this version starts with the other prefix.
*
@ -174,6 +198,13 @@ namespace mamba::specs
std::size_t m_epoch = 0;
};
auto operator==(const Version& left, const Version& other) -> bool;
auto operator!=(const Version& left, const Version& other) -> bool;
auto operator<(const Version& left, const Version& other) -> bool;
auto operator<=(const Version& left, const Version& other) -> bool;
auto operator>(const Version& left, const Version& other) -> bool;
auto operator>=(const Version& left, const Version& other) -> bool;
namespace version_literals
{
auto operator""_v(const char* str, std::size_t len) -> Version;
@ -189,6 +220,15 @@ struct fmt::formatter<mamba::specs::VersionPartAtom>
-> decltype(ctx.out());
};
template <>
struct fmt::formatter<mamba::specs::VersionPart>
{
auto parse(format_parse_context& ctx) -> decltype(ctx.begin());
auto format(const ::mamba::specs::VersionPart atom, format_context& ctx) const
-> decltype(ctx.out());
};
template <>
struct fmt::formatter<mamba::specs::Version>
{

View File

@ -48,6 +48,92 @@ namespace mamba::specs
{
return compare_three_way(std::strcmp(a.c_str(), b.c_str()), 0);
}
/**
* Compare two ranges where some trailing elements can be considered as empty.
*
* If ``0`` is considered "empty" then all the ranges ``[1, 2] and ``[1, 2, 0]``,
* ``[1, 2, 0, 0]`` are considered equal, however ``[1, 2]`` and ``[1, 0, 2]`` are not.
* Similarly ``[1, 1] is less than ``[1, 2, 0]`` but more than ``[1, 1, -1]``
* because ``-1 < 0``.
*
* @return The comparison between the two sequences
* @return The first index where the two sequences diverge.
*/
template <typename Iter1, typename Iter2, typename Empty1, typename Empty2, typename Cmp>
constexpr auto lexicographical_compare_three_way_trailing(
Iter1 first1,
Iter1 last1,
Iter2 first2,
Iter2 last2,
const Empty1& empty1,
const Empty2& empty2,
Cmp comp
) -> std::pair<strong_ordering, std::size_t>
{
assert(std::distance(first1, last1) >= 0);
assert(std::distance(first2, last2) >= 0);
auto iter1 = first1;
auto iter2 = first2;
for (; (iter1 != last1) && (iter2 != last2); ++iter1, ++iter2)
{
if (auto c = comp(*iter1, *iter2); c != strong_ordering::equal)
{
return { c, static_cast<std::size_t>(std::distance(first1, iter1)) };
}
}
// They have the same leading elements but 1 has more elements
// We do a lexicographic comparison with an infinite sequence of empties
if ((iter1 != last1))
{
for (; iter1 != last1; ++iter1)
{
if (auto c = comp(*iter1, empty2); c != strong_ordering::equal)
{
return { c, static_cast<std::size_t>(std::distance(first1, iter1)) };
}
}
}
// first2 != last2
// They have the same leading elements but 2 has more elements
// We do a lexicographic comparison with an infinite sequence of empties
if ((iter2 != last2))
{
for (; iter2 != last2; ++iter2)
{
if (auto c = comp(empty1, *iter2); c != strong_ordering::equal)
{
return { c, static_cast<std::size_t>(std::distance(first2, iter2)) };
}
}
}
// They have the same elements
return { strong_ordering::equal, static_cast<std::size_t>(std::distance(first1, iter1)) };
}
template <typename Iter1, typename Iter2, typename Empty, typename Cmp>
constexpr auto lexicographical_compare_three_way_trailing(
Iter1 first1,
Iter1 last1,
Iter2 first2,
Iter2 last2,
const Empty& empty,
Cmp comp
) -> std::pair<strong_ordering, std::size_t>
{
return lexicographical_compare_three_way_trailing(
first1,
last1,
first2,
last2,
empty,
empty,
comp
);
}
}
/***************************************
@ -142,38 +228,38 @@ namespace mamba::specs
}
}
auto VersionPartAtom::operator==(const VersionPartAtom& other) const -> bool
auto operator==(const VersionPartAtom& left, const VersionPartAtom& right) -> bool
{
// More efficient than three way comparison because of edge cases
auto attrs = [](const VersionPartAtom& a) -> std::tuple<std::size_t, const std::string&>
{ return { a.numeral(), a.literal() }; };
return attrs(*this) == attrs(other);
return attrs(left) == attrs(right);
}
auto VersionPartAtom::operator!=(const VersionPartAtom& other) const -> bool
auto operator!=(const VersionPartAtom& left, const VersionPartAtom& right) -> bool
{
// More efficient than three way comparison
return !(*this == other);
return !(left == right);
}
auto VersionPartAtom::operator<(const VersionPartAtom& other) const -> bool
auto operator<(const VersionPartAtom& left, const VersionPartAtom& right) -> bool
{
return compare_three_way(*this, other) == strong_ordering::less;
return compare_three_way(left, right) == strong_ordering::less;
}
auto VersionPartAtom::operator<=(const VersionPartAtom& other) const -> bool
auto operator<=(const VersionPartAtom& left, const VersionPartAtom& right) -> bool
{
return compare_three_way(*this, other) != strong_ordering::greater;
return compare_three_way(left, right) != strong_ordering::greater;
}
auto VersionPartAtom::operator>(const VersionPartAtom& other) const -> bool
auto operator>(const VersionPartAtom& left, const VersionPartAtom& right) -> bool
{
return compare_three_way(*this, other) == strong_ordering::greater;
return compare_three_way(left, right) == strong_ordering::greater;
}
auto VersionPartAtom::operator>=(const VersionPartAtom& other) const -> bool
auto operator>=(const VersionPartAtom& left, const VersionPartAtom& other) -> bool
{
return compare_three_way(*this, other) != strong_ordering::less;
return compare_three_way(left, other) != strong_ordering::less;
}
}
@ -198,6 +284,124 @@ fmt::formatter<mamba::specs::VersionPartAtom>::format(
return fmt::format_to(ctx.out(), "{}{}", atom.numeral(), atom.literal());
}
namespace mamba::specs
{
/***********************************
* Implementation of VersionPart *
***********************************/
VersionPart::VersionPart()
: atoms()
, implicit_leading_zero(false)
{
}
VersionPart::VersionPart(std::initializer_list<VersionPartAtom> init)
: atoms(init)
{
}
VersionPart::VersionPart(std::vector<VersionPartAtom> p_atoms, bool p_implicit_leading_zero)
: atoms(std::move(p_atoms))
, implicit_leading_zero(p_implicit_leading_zero)
{
}
auto VersionPart::str() const -> std::string
{
return fmt::format("{}", *this);
}
namespace
{
template <>
auto compare_three_way(const VersionPart& a, const VersionPart& b) -> strong_ordering
{
return lexicographical_compare_three_way_trailing(
a.atoms.cbegin(),
a.atoms.cend(),
b.atoms.cbegin(),
b.atoms.cend(),
VersionPartAtom{},
[](const auto& x, const auto& y) { return compare_three_way(x, y); }
).first;
}
}
auto operator==(const VersionPart& left, const VersionPart& right) -> bool
{
return compare_three_way(left, right) == strong_ordering::equal;
}
auto operator!=(const VersionPart& left, const VersionPart& right) -> bool
{
return !(left == right);
}
auto operator<(const VersionPart& left, const VersionPart& right) -> bool
{
return compare_three_way(left, right) == strong_ordering::less;
}
auto operator<=(const VersionPart& left, const VersionPart& right) -> bool
{
return compare_three_way(left, right) != strong_ordering::greater;
}
auto operator>(const VersionPart& left, const VersionPart& right) -> bool
{
return compare_three_way(left, right) == strong_ordering::greater;
}
auto operator>=(const VersionPart& left, const VersionPart& right) -> bool
{
return compare_three_way(left, right) != strong_ordering::less;
}
}
auto
fmt::formatter<mamba::specs::VersionPart>::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::VersionPart>::format(
const ::mamba::specs::VersionPart part,
format_context& ctx
) const -> decltype(ctx.out())
{
auto out = ctx.out();
if (part.atoms.empty())
{
return out;
}
const auto& first = part.atoms.front();
if (part.implicit_leading_zero && (first.numeral() == 0) && (!first.literal().empty()))
{
// The implicit leading zero is omitted
out = fmt::format_to(out, "{}", first.literal());
}
else
{
out = fmt::format_to(out, "{}", first);
}
const auto n_atoms = part.atoms.size();
for (std::size_t i = 1; i < n_atoms; ++i)
{
out = fmt::format_to(out, "{}", part.atoms[i]);
}
return out;
}
namespace mamba::specs
{
@ -248,104 +452,6 @@ namespace mamba::specs
namespace
{
/**
* Compare two ranges where some trailing elements can be considered as empty.
*
* If ``0`` is considered "empty" then all the ranges ``[1, 2] and ``[1, 2, 0]``,
* ``[1, 2, 0, 0]`` are considered equal, however ``[1, 2]`` and ``[1, 0, 2]`` are not.
* Similarly ``[1, 1] is less than ``[1, 2, 0]`` but more than ``[1, 1, -1]``
* because ``-1 < 0``.
*
* @return The comparison between the two sequences
* @return The first index where the two sequences diverge.
*/
template <typename Iter1, typename Iter2, typename Empty1, typename Empty2, typename Cmp>
constexpr auto lexicographical_compare_three_way_trailing(
Iter1 first1,
Iter1 last1,
Iter2 first2,
Iter2 last2,
const Empty1& empty1,
const Empty2& empty2,
Cmp comp
) -> std::pair<strong_ordering, std::size_t>
{
assert(std::distance(first1, last1) >= 0);
assert(std::distance(first2, last2) >= 0);
auto iter1 = first1;
auto iter2 = first2;
for (; (iter1 != last1) && (iter2 != last2); ++iter1, ++iter2)
{
if (auto c = comp(*iter1, *iter2); c != strong_ordering::equal)
{
return { c, static_cast<std::size_t>(std::distance(first1, iter1)) };
}
}
// They have the same leading elements but 1 has more elements
// We do a lexicographic comparison with an infinite sequence of empties
if ((iter1 != last1))
{
for (; iter1 != last1; ++iter1)
{
if (auto c = comp(*iter1, empty2); c != strong_ordering::equal)
{
return { c, static_cast<std::size_t>(std::distance(first1, iter1)) };
}
}
}
// first2 != last2
// They have the same leading elements but 2 has more elements
// We do a lexicographic comparison with an infinite sequence of empties
if ((iter2 != last2))
{
for (; iter2 != last2; ++iter2)
{
if (auto c = comp(empty1, *iter2); c != strong_ordering::equal)
{
return { c, static_cast<std::size_t>(std::distance(first2, iter2)) };
}
}
}
// They have the same elements
return { strong_ordering::equal, static_cast<std::size_t>(std::distance(first1, iter1)) };
}
template <typename Iter1, typename Iter2, typename Empty, typename Cmp>
constexpr auto lexicographical_compare_three_way_trailing(
Iter1 first1,
Iter1 last1,
Iter2 first2,
Iter2 last2,
const Empty& empty,
Cmp comp
) -> std::pair<strong_ordering, std::size_t>
{
return lexicographical_compare_three_way_trailing(
first1,
last1,
first2,
last2,
empty,
empty,
comp
);
}
template <>
auto compare_three_way(const VersionPart& a, const VersionPart& b) -> strong_ordering
{
return lexicographical_compare_three_way_trailing(
a.cbegin(),
a.cend(),
b.cbegin(),
b.cend(),
VersionPartAtom{},
[](const auto& x, const auto& y) { return compare_three_way(x, y); }
).first;
}
template <>
auto compare_three_way(const CommonVersion& a, const CommonVersion& b) -> strong_ordering
{
@ -375,34 +481,34 @@ namespace mamba::specs
}
// TODO(C++20) use operator<=> to simplify code and improve operator<=
auto Version::operator==(const Version& other) const -> bool
auto operator==(const Version& left, const Version& right) -> bool
{
return compare_three_way(*this, other) == strong_ordering::equal;
return compare_three_way(left, right) == strong_ordering::equal;
}
auto Version::operator!=(const Version& other) const -> bool
auto operator!=(const Version& left, const Version& right) -> bool
{
return !(*this == other);
return !(left == right);
}
auto Version::operator<(const Version& other) const -> bool
auto operator<(const Version& left, const Version& right) -> bool
{
return compare_three_way(*this, other) == strong_ordering::less;
return compare_three_way(left, right) == strong_ordering::less;
}
auto Version::operator<=(const Version& other) const -> bool
auto operator<=(const Version& left, const Version& right) -> bool
{
return compare_three_way(*this, other) != strong_ordering::greater;
return compare_three_way(left, right) != strong_ordering::greater;
}
auto Version::operator>(const Version& other) const -> bool
auto operator>(const Version& left, const Version& right) -> bool
{
return compare_three_way(*this, other) == strong_ordering::greater;
return compare_three_way(left, right) == strong_ordering::greater;
}
auto Version::operator>=(const Version& other) const -> bool
auto operator>=(const Version& left, const Version& right) -> bool
{
return compare_three_way(*this, other) != strong_ordering::less;
return compare_three_way(left, right) != strong_ordering::less;
}
namespace
@ -444,10 +550,10 @@ namespace mamba::specs
auto starts_with_three_way(const VersionPart& a, const VersionPart& b) -> strong_ordering
{
return lexicographical_compare_three_way_trailing(
a.cbegin(),
a.cend(),
b.cbegin(),
b.cend(),
a.atoms.cbegin(),
a.atoms.cend(),
b.atoms.cbegin(),
b.atoms.cend(),
VersionPartAtom{},
AlwaysEqual{},
[](const auto& x, const auto& y) { return starts_with_three_way(x, y); }
@ -615,11 +721,13 @@ namespace mamba::specs
{
assert(!str.empty());
VersionPart atoms = {};
auto atoms = VersionPart();
atoms.implicit_leading_zero = !util::is_digit(str.front());
while (!str.empty())
{
atoms.emplace_back();
std::tie(atoms.back(), str) = parse_leading_part_atom(str);
atoms.atoms.emplace_back();
std::tie(atoms.atoms.back(), str) = parse_leading_part_atom(str);
}
return atoms;
}
@ -843,10 +951,7 @@ fmt::formatter<mamba::specs::Version>::format(const ::mamba::specs::Version v, f
}
else
{
for (const auto& atom : version[i])
{
l_out = fmt::format_to(l_out, "{}", atom);
}
l_out = fmt::format_to(l_out, "{}", version[i]);
}
}
else

View File

@ -160,10 +160,10 @@ namespace
auto ms = MatchSpec::parse("mingw-w64-ucrt-x86_64-crt-git v12.0.0.r2.ggc561118da h707e725_0")
.value();
REQUIRE(ms.name().str() == "mingw-w64-ucrt-x86_64-crt-git");
REQUIRE(ms.version().str() == "==0v12.0.0.0r2.0ggc561118da");
REQUIRE(ms.version().str() == "==v12.0.0.r2.ggc561118da");
REQUIRE(ms.build_string().str() == "h707e725_0");
REQUIRE(ms.build_number().is_explicitly_free());
REQUIRE(ms.str() == "mingw-w64-ucrt-x86_64-crt-git==0v12.0.0.0r2.0ggc561118da=h707e725_0");
REQUIRE(ms.str() == "mingw-w64-ucrt-x86_64-crt-git==v12.0.0.r2.ggc561118da=h707e725_0");
}
SECTION("openblas 0.2.18|0.2.18.*.")
@ -216,20 +216,20 @@ namespace
{
auto ms = MatchSpec::parse("disperse=v0.9.24").value();
REQUIRE(ms.name().str() == "disperse");
REQUIRE(ms.version().str() == "=0v0.9.24");
REQUIRE(ms.version().str() == "=v0.9.24");
REQUIRE(ms.build_string().is_explicitly_free());
REQUIRE(ms.build_number().is_explicitly_free());
REQUIRE(ms.str() == "disperse=0v0.9.24");
REQUIRE(ms.str() == "disperse=v0.9.24");
}
SECTION("disperse v0.9.24")
{
auto ms = MatchSpec::parse("disperse v0.9.24").value();
REQUIRE(ms.name().str() == "disperse");
REQUIRE(ms.version().str() == "==0v0.9.24");
REQUIRE(ms.version().str() == "==v0.9.24");
REQUIRE(ms.build_string().is_explicitly_free());
REQUIRE(ms.build_number().is_explicitly_free());
REQUIRE(ms.str() == "disperse==0v0.9.24");
REQUIRE(ms.str() == "disperse==v0.9.24");
}
SECTION("foo V0.9.24")
@ -252,10 +252,10 @@ namespace
{
auto ms = MatchSpec::parse("foo=V0.9.24").value();
REQUIRE(ms.name().str() == "foo");
REQUIRE(ms.version().str() == "=0v0.9.24");
REQUIRE(ms.version().str() == "=v0.9.24");
REQUIRE(ms.build_string().is_explicitly_free());
REQUIRE(ms.build_number().is_explicitly_free());
REQUIRE(ms.str() == "foo=0v0.9.24");
REQUIRE(ms.str() == "foo=v0.9.24");
}
SECTION("numpy 1.7*")
@ -396,7 +396,7 @@ namespace
};
auto ms = MatchSpec::parse(str).value();
REQUIRE(ms.name().str() == "conda");
REQUIRE(ms.version().str() == "==4.3.21.0post699+1dab973"); // Note the ``.0post``
REQUIRE(ms.version().str() == "==4.3.21.post699+1dab973");
REQUIRE(ms.build_string().str() == "py36h4a561cd_0");
REQUIRE(ms.str() == str);
}
@ -673,11 +673,9 @@ namespace
REQUIRE(ms.channel().value().str() == "conda-canary[linux-64]");
REQUIRE(ms.platforms().value().get() == MatchSpec::platform_set{ "linux-64" });
REQUIRE(ms.name().str() == "conda");
REQUIRE(ms.version().str() == "==4.3.21.0post699+1dab973"); // Not ``.0post`` diff
REQUIRE(ms.version().str() == "==4.3.21.post699+1dab973");
REQUIRE(ms.build_string().str() == "py36h4a561cd_0");
REQUIRE(
ms.str() == "conda-canary[linux-64]::conda==4.3.21.0post699+1dab973=py36h4a561cd_0"
);
REQUIRE(ms.str() == "conda-canary[linux-64]::conda==4.3.21.post699+1dab973=py36h4a561cd_0");
}
SECTION("libblas[build=^.*(accelerate|mkl)$]")

View File

@ -12,12 +12,13 @@
#include <fmt/format.h>
#include "mamba/specs/version.hpp"
#include "mamba/util/string.hpp"
using namespace mamba::specs;
namespace
{
TEST_CASE("atom_comparison", "[mamba::specs][mamba::specs::Version]")
TEST_CASE("VersionPartAtom", "[mamba::specs][mamba::specs::Version]")
{
// No literal
REQUIRE(VersionPartAtom(1) == VersionPartAtom(1, ""));
@ -37,20 +38,20 @@ namespace
REQUIRE(VersionPartAtom(1, "a") >= VersionPartAtom(1, "dev"));
// clang-format off
auto sorted_atoms = std::array{
VersionPartAtom{ 1, "*" },
VersionPartAtom{ 1, "dev" },
VersionPartAtom{ 1, "_" },
VersionPartAtom{ 1, "a" },
VersionPartAtom{ 1, "alpha" },
VersionPartAtom{ 1, "b" },
VersionPartAtom{ 1, "beta" },
VersionPartAtom{ 1, "c" },
VersionPartAtom{ 1, "r" },
VersionPartAtom{ 1, "rc" },
VersionPartAtom{ 1, "" },
VersionPartAtom{ 1, "post" },
};
auto const sorted_atoms = std::array{
VersionPartAtom{ 1, "*" },
VersionPartAtom{ 1, "dev" },
VersionPartAtom{ 1, "_" },
VersionPartAtom{ 1, "a" },
VersionPartAtom{ 1, "alpha" },
VersionPartAtom{ 1, "b" },
VersionPartAtom{ 1, "beta" },
VersionPartAtom{ 1, "c" },
VersionPartAtom{ 1, "r" },
VersionPartAtom{ 1, "rc" },
VersionPartAtom{ 1, "" },
VersionPartAtom{ 1, "post" },
};
// clang-format on
// Strict ordering
@ -59,18 +60,55 @@ namespace
REQUIRE(std::adjacent_find(sorted_atoms.cbegin(), sorted_atoms.cend()) == sorted_atoms.cend());
}
TEST_CASE("atom_format", "[mamba::specs][mamba::specs::Version]")
TEST_CASE("VersionPartAtom::str", "[mamba::specs][mamba::specs::Version]")
{
REQUIRE(VersionPartAtom(1, "dev").str() == "1dev");
REQUIRE(VersionPartAtom(2).str() == "2");
}
TEST_CASE("version_comparison", "[mamba::specs][mamba::specs::Version]")
TEST_CASE("VersionPart", "[mamba::specs][mamba::specs::Version]")
{
// clang-format off
REQUIRE(VersionPart{{1, "dev"}} == VersionPart{{1, "dev"}});
REQUIRE(VersionPart{{1, "dev"}} == VersionPart{{1, "dev"}, {0, ""}});
REQUIRE(VersionPart{{1, "dev"}, {2, ""}} == VersionPart{{1, "dev"}, {2, ""}});
REQUIRE(VersionPart({{0, "dev"}, {2, ""}}, true) == VersionPart{{0, "dev"}, {2, ""}});
REQUIRE(VersionPart{{0, "dev"} } != VersionPart{{0, "dev"}, {2, ""}});
auto const sorted_parts = std::array{
VersionPart{{0, ""}},
VersionPart{{1, "dev"}, {0, "alpha"}},
VersionPart{{1, "dev"}},
VersionPart{{1, "dev"}, {1, "dev"}},
VersionPart{{2, "dev"}, {1, "dev"}},
VersionPart{{2, ""}},
VersionPart{{2, ""}, {0, "post"}},
};
// clang-format on
// Strict ordering
REQUIRE(std::is_sorted(sorted_parts.cbegin(), sorted_parts.cend()));
// None compare equal (given the is_sorted assumption)
REQUIRE(std::adjacent_find(sorted_parts.cbegin(), sorted_parts.cend()) == sorted_parts.cend());
}
TEST_CASE("VersionPart::str", "[mamba::specs][mamba::specs::Version]")
{
REQUIRE(VersionPart{ { 1, "dev" } }.str() == "1dev");
REQUIRE(VersionPart{ { 1, "dev" }, { 2, "" } }.str() == "1dev2");
REQUIRE(VersionPart{ { 1, "dev" }, { 2, "foo" }, { 33, "bar" } }.str() == "1dev2foo33bar");
REQUIRE(VersionPart({ { 0, "dev" }, { 2, "" } }, false).str() == "0dev2");
REQUIRE(VersionPart({ { 0, "dev" }, { 2, "" } }, true).str() == "dev2");
REQUIRE(VersionPart({ { 0, "dev" } }, true).str() == "dev");
REQUIRE(VersionPart({ { 0, "" } }, true).str() == "0");
}
TEST_CASE("Version cmp", "[mamba::specs][mamba::specs::Version]")
{
auto v = Version(0, { { { 1, "post" } } });
REQUIRE(v.version().size() == 1);
REQUIRE(v.version().front().size() == 1);
REQUIRE(v.version().front().front() == VersionPartAtom(1, "post"));
REQUIRE(v.version().front().atoms.size() == 1);
REQUIRE(v.version().front().atoms.front() == VersionPartAtom(1, "post"));
// Same empty 0!1post version
REQUIRE(Version(0, { { { 1, "post" } } }) == Version(0, { { { 1, "post" } } }));
@ -188,7 +226,7 @@ namespace
}
}
TEST_CASE("compatible_with", "[mamba::specs][mamba::specs::Version]")
TEST_CASE("Version compatible_with", "[mamba::specs][mamba::specs::Version]")
{
SECTION("positive")
{
@ -303,7 +341,7 @@ namespace
}
}
TEST_CASE("version_format", "[mamba::specs][mamba::specs::Version]")
TEST_CASE("Version::str", "[mamba::specs][mamba::specs::Version]")
{
SECTION("11a0post.3.4dev")
{
@ -357,65 +395,65 @@ namespace
*
* @see https://github.com/conda/conda/blob/main/tests/models/test_version.py
*/
TEST_CASE("Version parse", "[mamba::specs][mamba::specs::Version]")
TEST_CASE("Version::parse", "[mamba::specs][mamba::specs::Version]")
{
// clang-format off
auto sorted_version = std::vector<std::pair<std::string_view, Version>>{
{"0.4", Version(0, {{{0}}, {{4}}})},
{"0.4.0", Version(0, {{{0}}, {{4}}, {{0}}})},
{"0.4.1a.vc11", Version(0, {{{0}}, {{4}}, {{1, "a"}}, {{0, "vc"}, {11}}})},
{"0.4.1.rc", Version(0, {{{0}}, {{4}}, {{1}}, {{0, "rc"}}})},
{"0.4.1.vc11", Version(0, {{{0}}, {{4}}, {{1}}, {{0, "vc"}, {11}}})},
{"0.4.1", Version(0, {{{0}}, {{4}}, {{1}}})},
{"0.5*", Version(0, {{{0}}, {{5, "*"}}})},
{"0.5a1", Version(0, {{{0}}, {{5, "a"}, {1}}})},
{"0.5b3", Version(0, {{{0}}, {{5, "b"}, {3}}})},
{"0.5C1", Version(0, {{{0}}, {{5, "c"}, {1}}})},
{"0.5z", Version(0, {{{0}}, {{5, "z"}}})},
{"0.5za", Version(0, {{{0}}, {{5, "za"}}})},
{"0.5", Version(0, {{{0}}, {{5}}})},
{"0.5_5", Version(0, {{{0}}, {{5}}, {{5}}})},
{"0.5-5", Version(0, {{{0}}, {{5}}, {{5}}})},
{"0.9.6", Version(0, {{{0}}, {{9}}, {{6}}})},
{"0.960923", Version(0, {{{0}}, {{960923}}})},
{"1.0", Version(0, {{{1}}, {{0}}})},
{"1.0.4a3", Version(0, {{{1}}, {{0}}, {{4, "a"}, {3}}})},
{"1.0.4b1", Version(0, {{{1}}, {{0}}, {{4, "b"}, {1}}})},
{"1.0.4", Version(0, {{{1}}, {{0}}, {{4}}})},
{"1.1dev1", Version(0, {{{1}}, {{1, "dev"}, {1}}})},
{"1.1_", Version(0, {{{1}}, {{1, "_"}}})},
{"1.1a1", Version(0, {{{1}}, {{1, "a"}, {1}}})},
{"1.1.dev1", Version(0, {{{1}}, {{1}}, {{0, "dev"}, {1}}})},
{"1.1.a1", Version(0, {{{1}}, {{1}}, {{0, "a"}, {1}}})},
{"1.1", Version(0, {{{1}}, {{1}}})},
{"1.1.post1", Version(0, {{{1}}, {{1}}, {{0, "post"}, {1}}})},
{"1.1.1dev1", Version(0, {{{1}}, {{1}}, {{1, "dev"}, {1}}})},
{"1.1.1rc1", Version(0, {{{1}}, {{1}}, {{1, "rc"}, {1}}})},
{"1.1.1", Version(0, {{{1}}, {{1}}, {{1}}})},
{"1.1.1post1", Version(0, {{{1}}, {{1}}, {{1, "post"}, {1}}})},
{"1.1post1", Version(0, {{{1}}, {{1, "post"}, {1}}})},
{"2g6", Version(0, {{{2, "g"}, {6}}})},
{"2.0b1pr0", Version(0, {{{2}}, {{0, "b"}, {1, "pr"}, {0}}})},
{"2.2be.ta29", Version(0, {{{2}}, {{2, "be"}}, {{0, "ta"}, {29}}})},
{"2.2be5ta29", Version(0, {{{2}}, {{2, "be"}, {5, "ta"}, {29}}})},
{"2.2beta29", Version(0, {{{2}}, {{2, "beta"}, {29}}})},
{"2.2.0.1", Version(0, {{{2}}, {{2}}, {{0}}, {{1}}})},
{"3.1.1.6", Version(0, {{{3}}, {{1}}, {{1}}, {{6}}})},
{"3.2.p.r0", Version(0, {{{3}}, {{2}}, {{0, "p"}}, {{0, "r"}, {0}}})},
{"3.2.pr0", Version(0, {{{3}}, {{2}}, {{0, "pr"}, {0}}})},
{"3.2.pr.1", Version(0, {{{3}}, {{2}}, {{0, "pr"}}, {{1}}})},
{"5.5.kw", Version(0, {{{5}}, {{5}}, {{0, "kw"}}})},
{"11g", Version(0, {{{11, "g"}}})},
{"14.3.1", Version(0, {{{14}}, {{3}}, {{1}}})},
{
"14.3.1.post26.g9d75ca2",
Version( 0, {{{14}}, {{3}}, {{1}}, {{0, "post"}, {26}}, {{0, "g"}, {9, "d"}, {75, "ca"}, {2}}})
},
{"1996.07.12", Version(0, {{{1996}}, {{7}}, {{12}}})},
{"1!0.4.1", Version(1, {{{0}}, {{4}}, {{1}}})},
{"1!3.1.1.6", Version(1, {{{3}}, {{1}}, {{1}}, {{6}}})},
{"2!0.4.1", Version(2, {{{0}}, {{4}}, {{1}}})},
};
auto const sorted_version = std::vector<std::pair<std::string_view, Version>>{
{"0.4", Version(0, {{{0}}, {{4}}})},
{"0.4.0", Version(0, {{{0}}, {{4}}, {{0}}})},
{"0.4.1a.vc11", Version(0, {{{0}}, {{4}}, {{1, "a"}}, {{0, "vc"}, {11}}})},
{"0.4.1.rc", Version(0, {{{0}}, {{4}}, {{1}}, {{0, "rc"}}})},
{"0.4.1.vc11", Version(0, {{{0}}, {{4}}, {{1}}, {{0, "vc"}, {11}}})},
{"0.4.1", Version(0, {{{0}}, {{4}}, {{1}}})},
{"0.5*", Version(0, {{{0}}, {{5, "*"}}})},
{"0.5a1", Version(0, {{{0}}, {{5, "a"}, {1}}})},
{"0.5b3", Version(0, {{{0}}, {{5, "b"}, {3}}})},
{"0.5C1", Version(0, {{{0}}, {{5, "c"}, {1}}})},
{"0.5z", Version(0, {{{0}}, {{5, "z"}}})},
{"0.5za", Version(0, {{{0}}, {{5, "za"}}})},
{"0.5", Version(0, {{{0}}, {{5}}})},
{"0.5_5", Version(0, {{{0}}, {{5}}, {{5}}})},
{"0.5-5", Version(0, {{{0}}, {{5}}, {{5}}})},
{"0.9.6", Version(0, {{{0}}, {{9}}, {{6}}})},
{"0.960923", Version(0, {{{0}}, {{960923}}})},
{"1.0", Version(0, {{{1}}, {{0}}})},
{"1.0.4a3", Version(0, {{{1}}, {{0}}, {{4, "a"}, {3}}})},
{"1.0.4b1", Version(0, {{{1}}, {{0}}, {{4, "b"}, {1}}})},
{"1.0.4", Version(0, {{{1}}, {{0}}, {{4}}})},
{"1.1dev1", Version(0, {{{1}}, {{1, "dev"}, {1}}})},
{"1.1_", Version(0, {{{1}}, {{1, "_"}}})},
{"1.1a1", Version(0, {{{1}}, {{1, "a"}, {1}}})},
{"1.1.dev1", Version(0, {{{1}}, {{1}}, {{0, "dev"}, {1}}})},
{"1.1.a1", Version(0, {{{1}}, {{1}}, {{0, "a"}, {1}}})},
{"1.1", Version(0, {{{1}}, {{1}}})},
{"1.1.post1", Version(0, {{{1}}, {{1}}, {{0, "post"}, {1}}})},
{"1.1.1dev1", Version(0, {{{1}}, {{1}}, {{1, "dev"}, {1}}})},
{"1.1.1rc1", Version(0, {{{1}}, {{1}}, {{1, "rc"}, {1}}})},
{"1.1.1", Version(0, {{{1}}, {{1}}, {{1}}})},
{"1.1.1post1", Version(0, {{{1}}, {{1}}, {{1, "post"}, {1}}})},
{"1.1post1", Version(0, {{{1}}, {{1, "post"}, {1}}})},
{"2g6", Version(0, {{{2, "g"}, {6}}})},
{"2.0b1pr0", Version(0, {{{2}}, {{0, "b"}, {1, "pr"}, {0}}})},
{"2.2be.ta29", Version(0, {{{2}}, {{2, "be"}}, {{0, "ta"}, {29}}})},
{"2.2be5ta29", Version(0, {{{2}}, {{2, "be"}, {5, "ta"}, {29}}})},
{"2.2beta29", Version(0, {{{2}}, {{2, "beta"}, {29}}})},
{"2.2.0.1", Version(0, {{{2}}, {{2}}, {{0}}, {{1}}})},
{"3.1.1.6", Version(0, {{{3}}, {{1}}, {{1}}, {{6}}})},
{"3.2.p.r0", Version(0, {{{3}}, {{2}}, {{0, "p"}}, {{0, "r"}, {0}}})},
{"3.2.pr0", Version(0, {{{3}}, {{2}}, {{0, "pr"}, {0}}})},
{"3.2.pr.1", Version(0, {{{3}}, {{2}}, {{0, "pr"}}, {{1}}})},
{"5.5.kw", Version(0, {{{5}}, {{5}}, {{0, "kw"}}})},
{"11g", Version(0, {{{11, "g"}}})},
{"14.3.1", Version(0, {{{14}}, {{3}}, {{1}}})},
{
"14.3.1.post26.g9d75ca2",
Version( 0, {{{14}}, {{3}}, {{1}}, {{0, "post"}, {26}}, {{0, "g"}, {9, "d"}, {75, "ca"}, {2}}})
},
{"1996.07.12", Version(0, {{{1996}}, {{7}}, {{12}}})},
{"1!0.4.1", Version(1, {{{0}}, {{4}}, {{1}}})},
{"1!3.1.1.6", Version(1, {{{3}}, {{1}}, {{1}}, {{6}}})},
{"2!0.4.1", Version(2, {{{0}}, {{4}}, {{1}}})},
};
// clang-format on
for (const auto& [raw, expected] : sorted_version)
{
@ -442,6 +480,11 @@ namespace
REQUIRE(Version::parse("0.4.a1").value() == Version::parse("0.4.0a1"));
REQUIRE(Version::parse("0.4.a1").value() != Version::parse("0.4.1a1"));
// Parse implicit zeros
REQUIRE(Version::parse("0.4.a1").value().version()[2].implicit_leading_zero);
REQUIRE(Version::parse("0.4.a1").value().str() == "0.4.a1");
REQUIRE(Version::parse("g56ffd88f").value().str() == "g56ffd88f");
// These are valid versions with the special '*' ordering AND they are also used as such
// with version globs in VersionSpec
REQUIRE(Version::parse("*") == Version(0, { { { 0, "*" } } }));
@ -456,7 +499,7 @@ namespace
REQUIRE(Version::parse("1.*") == Version(0, { { { 1, "" } }, { { 0, "*" } } }));
}
TEST_CASE("parse_invalid", "[mamba::specs][mamba::specs::Version]")
TEST_CASE("Version::parse negative", "[mamba::specs][mamba::specs::Version]")
{
// Wrong epoch
REQUIRE_FALSE(Version::parse("!1.1").has_value());

View File

@ -20,6 +20,7 @@
#include "mamba/specs/unresolved_channel.hpp"
#include "mamba/specs/version.hpp"
#include "mamba/specs/version_spec.hpp"
#include "mamba/version.hpp"
#include "bind_utils.hpp"
#include "bindings.hpp"
@ -27,8 +28,10 @@
#include "flat_set_caster.hpp"
#include "weakening_map_bind.hpp"
PYBIND11_MAKE_OPAQUE(mamba::specs::VersionPart);
PYBIND11_MAKE_OPAQUE(mamba::specs::CommonVersion);
using OldVersionPart = std::vector<mamba::specs::VersionPartAtom>;
using OldCommonVersion = std::vector<OldVersionPart>;
PYBIND11_MAKE_OPAQUE(OldVersionPart);
PYBIND11_MAKE_OPAQUE(OldCommonVersion);
namespace mambapy
{
@ -529,11 +532,28 @@ namespace mambapy
.def("__copy__", &copy<VersionPartAtom>)
.def("__deepcopy__", &deepcopy<VersionPartAtom>, py::arg("memo"));
// Type made opaque at the top of this file
py::bind_vector<VersionPart>(m, "VersionPart");
// TODO(3.0): Align the Python API of VersionPart (and thus break CommonVersion,
// Version::version, Version::local), with the C++ one.
static_assert(LIBMAMBA_VERSION_MAJOR < 3, "Take the major release opportunity to clean APIs.");
// Type made opaque at the top of this file
py::bind_vector<CommonVersion>(m, "CommonVersion");
py::bind_vector<OldVersionPart>(m, "VersionPart");
// Type made opaque at the top of this file
py::bind_vector<OldCommonVersion>(m, "CommonVersion");
constexpr auto common_version_backport = [](const CommonVersion& version) -> OldCommonVersion
{
auto old = OldCommonVersion();
old.reserve(version.size());
std::transform(
version.cbegin(),
version.cend(),
std::back_inserter(old),
[](const auto& a) { return a.atoms; }
);
return old;
};
py::class_<Version>(m, "Version")
.def_readonly_static("epoch_delim", &Version::epoch_delim)
@ -549,8 +569,14 @@ namespace mambapy
py::arg("local") = CommonVersion()
)
.def_property_readonly("epoch", &Version::epoch)
.def_property_readonly("version", &Version::version)
.def_property_readonly("local", &Version::local)
.def_property_readonly(
"version",
[=](const Version& self) { return common_version_backport(self.version()); }
)
.def_property_readonly(
"local",
[=](const Version& self) { return common_version_backport(self.local()); }
)
.def("starts_with", &Version::starts_with, py::arg("prefix"))
.def("compatible_with", &Version::compatible_with, py::arg("older"), py::arg("level"))
.def("__str__", [](const Version& v) { return v.str(); })

View File

@ -598,8 +598,10 @@ def test_VersionPart():
VersionPartAtom = libmambapy.specs.VersionPartAtom
VersionPart = libmambapy.specs.VersionPart
p = VersionPart([VersionPartAtom(1, "a"), VersionPartAtom(3)])
assert len(p) == 2
atoms = [VersionPartAtom(1, "a"), VersionPartAtom(3)]
p = VersionPart(atoms)
assert len(p) == len(atoms)
assert p == p
def test_CommonVersion():