Handle regex in build string (#3239)

This commit is contained in:
Antoine Prouvost 2024-03-20 18:52:22 +01:00 committed by GitHub
parent cad6793e21
commit 7942d3feaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 657 additions and 30 deletions

View File

@ -166,11 +166,13 @@ set(
${LIBMAMBA_SOURCE_DIR}/specs/authentication_info.cpp
${LIBMAMBA_SOURCE_DIR}/specs/build_number_spec.cpp
${LIBMAMBA_SOURCE_DIR}/specs/channel.cpp
${LIBMAMBA_SOURCE_DIR}/specs/chimera_string_spec.cpp
${LIBMAMBA_SOURCE_DIR}/specs/conda_url.cpp
${LIBMAMBA_SOURCE_DIR}/specs/glob_spec.cpp
${LIBMAMBA_SOURCE_DIR}/specs/match_spec.cpp
${LIBMAMBA_SOURCE_DIR}/specs/package_info.cpp
${LIBMAMBA_SOURCE_DIR}/specs/platform.cpp
${LIBMAMBA_SOURCE_DIR}/specs/regex_spec.cpp
${LIBMAMBA_SOURCE_DIR}/specs/repo_data.cpp
${LIBMAMBA_SOURCE_DIR}/specs/unresolved_channel.cpp
${LIBMAMBA_SOURCE_DIR}/specs/version.cpp

View File

@ -0,0 +1,69 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_SPECS_CHIMERA_STRING_SPEC
#define MAMBA_SPECS_CHIMERA_STRING_SPEC
#include <string_view>
#include <variant>
#include <fmt/core.h>
#include "mamba/specs/error.hpp"
#include "mamba/specs/glob_spec.hpp"
#include "mamba/specs/regex_spec.hpp"
namespace mamba::specs
{
/**
* A matcher for either a glob or a regex expression.
*/
class ChimeraStringSpec
{
public:
using Chimera = std::variant<GlobSpec, RegexSpec>;
[[nodiscard]] static auto parse(std::string pattern) -> expected_parse_t<ChimeraStringSpec>;
ChimeraStringSpec();
explicit ChimeraStringSpec(Chimera spec);
[[nodiscard]] auto contains(std::string_view str) const -> bool;
/**
* Return true if the spec will match true on any input.
*/
[[nodiscard]] auto is_explicitly_free() const -> bool;
/**
* Return true if the spec will match exactly one input.
*/
[[nodiscard]] auto is_exact() const -> bool;
/**
* Return true if the spec is a glob and not a regex.
*/
[[nodiscard]] auto is_glob() const -> bool;
[[nodiscard]] auto str() const -> const std::string&;
private:
Chimera m_spec;
};
}
template <>
struct fmt::formatter<mamba::specs::ChimeraStringSpec>
{
auto parse(format_parse_context& ctx) -> decltype(ctx.begin());
auto format(const ::mamba::specs::ChimeraStringSpec& spec, format_context& ctx)
-> decltype(ctx.out());
};
#endif

View File

@ -15,6 +15,7 @@
#include <fmt/core.h>
#include "mamba/specs/build_number_spec.hpp"
#include "mamba/specs/chimera_string_spec.hpp"
#include "mamba/specs/error.hpp"
#include "mamba/specs/glob_spec.hpp"
#include "mamba/specs/unresolved_channel.hpp"
@ -31,7 +32,7 @@ namespace mamba::specs
public:
using NameSpec = GlobSpec;
using BuildStringSpec = GlobSpec;
using BuildStringSpec = ChimeraStringSpec;
using platform_set = typename UnresolvedChannel::platform_set;
using platform_set_const_ref = std::reference_wrapper<const platform_set>;
using string_set = typename util::flat_set<std::string>;

View File

@ -0,0 +1,65 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_SPECS_REGEX_SPEC
#define MAMBA_SPECS_REGEX_SPEC
#include <regex>
#include <string>
#include <string_view>
#include <fmt/core.h>
#include "mamba/specs/error.hpp"
namespace mamba::specs
{
/**
* A matcher for regex expression.
*/
class RegexSpec
{
public:
inline static constexpr std::string_view free_pattern = ".*";
inline static constexpr char pattern_start = '^';
inline static constexpr char pattern_end = '$';
[[nodiscard]] static auto parse(std::string pattern) -> expected_parse_t<RegexSpec>;
RegexSpec();
RegexSpec(std::regex pattern, std::string raw_pattern);
[[nodiscard]] auto contains(std::string_view str) const -> bool;
/**
* Return true if the spec will match true on any input.
*/
[[nodiscard]] auto is_explicitly_free() const -> bool;
/**
* Return true if the spec will match exaclty one input.
*/
[[nodiscard]] auto is_exact() const -> bool;
[[nodiscard]] auto str() const -> const std::string&;
private:
std::regex m_pattern;
std::string m_raw_pattern;
};
}
template <>
struct fmt::formatter<mamba::specs::RegexSpec>
{
auto parse(format_parse_context& ctx) -> decltype(ctx.begin());
auto format(const ::mamba::specs::RegexSpec& spec, format_context& ctx) -> decltype(ctx.out());
};
#endif

View File

@ -76,7 +76,9 @@ namespace mamba
}
if (!pkg.build_string.empty())
{
out.set_build_string(specs::MatchSpec::BuildStringSpec(pkg.build_string));
out.set_build_string(
specs::MatchSpec::BuildStringSpec(specs::GlobSpec(pkg.build_string))
);
}
return out;
}

View File

@ -1171,7 +1171,9 @@ namespace mamba::solver::libsolv
));
}
);
ms.set_build_string(specs::MatchSpec::BuildStringSpec(std::string(s.build_string())));
ms.set_build_string(
specs::MatchSpec::BuildStringSpec(specs::GlobSpec(std::string(s.build_string())))
);
ms.set_build_number(
specs::BuildNumberSpec(specs::BuildNumberPredicate::make_equal_to(s.build_number()))
);

View File

@ -303,7 +303,7 @@ namespace mamba::solver
using TT = std::remove_cv_t<std::remove_reference_t<T>>;
using Build = decltype(std::invoke(&TT::build_string, std::forward<T>(e)));
Build bld = std::invoke(&TT::build_string, std::forward<T>(e));
if constexpr (std::is_same_v<std::decay_t<decltype(bld)>, specs::GlobSpec>)
if constexpr (std::is_same_v<std::decay_t<decltype(bld)>, specs::ChimeraStringSpec>)
{
return std::forward<Build>(bld).str();
}

View File

@ -0,0 +1,146 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <algorithm>
#include <cassert>
#include <type_traits>
#include <fmt/format.h>
#include "mamba/specs/chimera_string_spec.hpp"
#include "mamba/specs/regex_spec.hpp"
#include "mamba/util/string.hpp"
namespace mamba::specs
{
namespace
{
[[nodiscard]] auto is_likely_regex(std::string_view pattern) -> bool
{
return util::starts_with(pattern, RegexSpec::pattern_start)
|| util::ends_with(pattern, RegexSpec::pattern_end);
}
[[nodiscard]] auto make_regex(std::string pattern) -> expected_parse_t<ChimeraStringSpec>
{
return RegexSpec::parse(std::move(pattern))
.transform(
[](RegexSpec&& spec) -> ChimeraStringSpec
{
// Chose a lighter implementation when possible
if (spec.is_explicitly_free())
{
return {};
}
if (spec.is_exact())
{
return ChimeraStringSpec{ GlobSpec(std::move(spec).str()) };
}
return ChimeraStringSpec{ std::move(spec) };
}
);
}
[[nodiscard]] auto is_likely_glob(std::string_view pattern) -> bool
{
constexpr auto is_likely_glob_char = [](char c) -> bool {
return util::is_alphanum(c) || (c == '-') || (c == '_')
|| (c == GlobSpec::glob_pattern);
};
return pattern.empty() || (pattern == GlobSpec::free_pattern)
|| std::all_of(pattern.cbegin(), pattern.cend(), is_likely_glob_char);
}
}
auto ChimeraStringSpec::parse(std::string pattern) -> expected_parse_t<ChimeraStringSpec>
{
if (is_likely_regex(pattern))
{
return make_regex(std::move(pattern));
}
if (is_likely_glob(pattern))
{
return { ChimeraStringSpec(GlobSpec(std::move(pattern))) };
}
return make_regex(pattern).or_else(
[&](const auto& /* error */) -> expected_parse_t<ChimeraStringSpec>
{ return { ChimeraStringSpec(GlobSpec(std::move(pattern))) }; }
);
}
ChimeraStringSpec::ChimeraStringSpec()
: ChimeraStringSpec(GlobSpec())
{
}
ChimeraStringSpec::ChimeraStringSpec(Chimera spec)
: m_spec(std::move(spec))
{
}
auto ChimeraStringSpec::contains(std::string_view str) const -> bool
{
return std::visit([&](const auto& s) { return s.contains(str); }, m_spec);
}
auto ChimeraStringSpec::is_explicitly_free() const -> bool
{
return std::visit(
[](const auto& s) -> bool
{
using S = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<S, GlobSpec>)
{
return s.is_free();
}
else if constexpr (std::is_same_v<S, RegexSpec>)
{
return s.is_explicitly_free();
}
// All variant cases are not handled.
assert(false);
},
m_spec
);
}
auto ChimeraStringSpec::is_exact() const -> bool
{
return std::visit([](const auto& s) { return s.is_exact(); }, m_spec);
}
auto ChimeraStringSpec::is_glob() const -> bool
{
return std::holds_alternative<GlobSpec>(m_spec);
}
auto ChimeraStringSpec::str() const -> const std::string&
{
return std::visit([](const auto& s) -> decltype(auto) { return s.str(); }, m_spec);
}
}
auto
fmt::formatter<mamba::specs::ChimeraStringSpec>::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::ChimeraStringSpec>::format(
const ::mamba::specs::ChimeraStringSpec& spec,
format_context& ctx
) -> decltype(ctx.out())
{
return fmt::format_to(ctx.out(), "{}", spec.str());
}

View File

@ -37,7 +37,15 @@ namespace mamba::specs
// Build string
auto [head, tail] = util::rsplit_once(strip_archive_extension(pkg), '-');
out.m_build_string = BuildStringSpec(std::string(tail));
auto maybe_build_string = BuildStringSpec::parse(std::string(tail));
if (maybe_build_string.has_value())
{
out.m_build_string = std::move(maybe_build_string).value();
}
else
{
return make_unexpected_parse(std::move(maybe_build_string).error());
}
if (!head.has_value())
{
@ -216,8 +224,9 @@ namespace mamba::specs
}
if ((attr == "build") || (attr == "build_string"))
{
spec.set_build_string(MatchSpec::BuildStringSpec(std::string(val)));
return {};
return MatchSpec::BuildStringSpec::parse(std::string(val))
.transform([&](MatchSpec::BuildStringSpec&& bs)
{ spec.set_build_string(std::move(bs)); });
}
if (attr == "version")
{
@ -575,14 +584,19 @@ namespace mamba::specs
auto maybe_ver = VersionSpec::parse(ver_str);
if (!maybe_ver)
{
return parse_error(maybe_ver.error().what());
return make_unexpected_parse(std::move(maybe_ver).error());
}
out.m_version = std::move(maybe_ver).value();
}
if (!bld_str.empty())
{
out.m_build_string = MatchSpec::BuildStringSpec(std::string(bld_str));
auto maybe_build_string = BuildStringSpec::parse(std::string(bld_str));
if (!maybe_build_string)
{
return make_unexpected_parse(std::move(maybe_build_string).error());
}
out.m_build_string = std::move(maybe_build_string).value();
}
return out;
@ -895,7 +909,7 @@ namespace mamba::specs
auto MatchSpec::conda_build_form() const -> std::string
{
const bool has_version = !m_version.is_explicitly_free();
const bool has_build_str = !m_build_string.is_free();
const bool has_build_str = !m_build_string.is_explicitly_free();
if (has_version)
{
if (has_build_str)
@ -945,6 +959,7 @@ namespace mamba::specs
// Glob in names and build_string are fine
return (version().expression_size() <= 3) // includes op so e.g. ``>3,<4``
&& build_number().is_explicitly_free() //
&& build_string().is_glob() //
&& !channel().has_value() //
&& filename().empty() //
&& !platforms().has_value() //
@ -959,9 +974,9 @@ namespace mamba::specs
[[nodiscard]] auto MatchSpec::is_only_package_name() const -> bool
{
return name().is_exact() //
&& version().is_explicitly_free() //
&& build_string().is_free() //
return name().is_exact() //
&& version().is_explicitly_free() //
&& build_string().is_explicitly_free() //
&& is_simple();
}
@ -1068,7 +1083,7 @@ fmt::formatter<::mamba::specs::MatchSpec>::format(
const bool is_complex_version = spec.version().expression_size() > 1;
const bool is_complex_build_string = !(
spec.build_string().is_exact() || spec.build_string().is_free()
spec.build_string().is_exact() || spec.build_string().is_explicitly_free()
);
// Any relation is complex, we'll write them all inside the attribute section.
@ -1076,7 +1091,7 @@ fmt::formatter<::mamba::specs::MatchSpec>::format(
// of the url.
if (!is_complex_version && !is_complex_build_string)
{
if (!spec.build_string().is_free())
if (!spec.build_string().is_explicitly_free())
{
out = fmt::format_to(out, "{}={}", spec.version(), spec.build_string());
}
@ -1111,7 +1126,7 @@ fmt::formatter<::mamba::specs::MatchSpec>::format(
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "version={0}{1}{0}", MatchSpec::prefered_quote, ver);
}
if (const auto& bs = spec.build_string(); !bs.is_free())
if (const auto& bs = spec.build_string(); !bs.is_explicitly_free())
{
ensure_bracket_open_or_comma();
out = fmt::format_to(out, "build={0}{1}{0}", MatchSpec::prefered_quote, bs);

View File

@ -0,0 +1,106 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <algorithm>
#include <cassert>
#include <fmt/format.h>
#include "mamba/specs/regex_spec.hpp"
#include "mamba/util/string.hpp"
namespace mamba::specs
{
namespace
{
template <typename Range, typename T>
[[nodiscard]] auto rng_contains(const Range& rng, const T& elem) -> bool
{
return std::find(rng.begin(), rng.end(), elem) != rng.end();
}
}
auto RegexSpec::parse(std::string pattern) -> expected_parse_t<RegexSpec>
{
// No other mean of getting parse result with ``std::regex``, but parse error need
// to be handled by ``tl::expected`` to be managed down the road.
try
{
auto regex = std::regex(pattern);
return { { std::move(regex), std::move(pattern) } };
}
catch (const std::regex_error& e)
{
return make_unexpected_parse(e.what());
}
}
RegexSpec::RegexSpec()
: RegexSpec(std::regex(free_pattern.data(), free_pattern.size()), std::string(free_pattern))
{
}
RegexSpec::RegexSpec(std::regex pattern, std::string raw_pattern)
: m_pattern(std::move(pattern))
, m_raw_pattern(std::move(raw_pattern))
{
// We force regex to start with `^` and end with `$` to simplify the multiple
// possible representations, and because this is the safest way we can make sure it is
// not a glob when serializing it.
if (!util::starts_with(m_raw_pattern, pattern_start))
{
m_raw_pattern.insert(m_raw_pattern.begin(), pattern_start);
}
if (!util::ends_with(m_raw_pattern, pattern_end))
{
m_raw_pattern.push_back(pattern_end);
}
}
auto RegexSpec::contains(std::string_view str) const -> bool
{
return std::regex_match(str.cbegin(), str.cend(), m_pattern);
}
auto RegexSpec::is_explicitly_free() const -> bool
{
assert(util::starts_with(m_raw_pattern, pattern_start));
assert(util::ends_with(m_raw_pattern, pattern_end));
return std::string_view(m_raw_pattern).substr(1, m_raw_pattern.size() - 2) == free_pattern;
}
auto RegexSpec::is_exact() const -> bool
{
constexpr auto no_special_meaning = [](char c)
{ return util::is_alphanum(c) || (c == '-') || (c == '_'); };
assert(util::starts_with(m_raw_pattern, pattern_start));
assert(util::ends_with(m_raw_pattern, pattern_end));
return std::all_of(m_raw_pattern.cbegin() + 1, m_raw_pattern.cend() - 1, no_special_meaning);
}
auto RegexSpec::str() const -> const std::string&
{
return m_raw_pattern;
}
}
auto
fmt::formatter<mamba::specs::RegexSpec>::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::RegexSpec>::format(const ::mamba::specs::RegexSpec& spec, format_context& ctx)
-> decltype(ctx.out())
{
return fmt::format_to(ctx.out(), "{}", spec.str());
}

View File

@ -38,11 +38,13 @@ set(
src/specs/test_authentication_info.cpp
src/specs/test_build_number_spec.cpp
src/specs/test_channel.cpp
src/specs/test_chimera_string_spec.cpp
src/specs/test_conda_url.cpp
src/specs/test_glob_spec.cpp
src/specs/test_match_spec.cpp
src/specs/test_package_info.cpp
src/specs/test_platform.cpp
src/specs/test_regex_spec.cpp
src/specs/test_repo_data.cpp
src/specs/test_unresolved_channel.cpp
src/specs/test_version.cpp

View File

@ -0,0 +1,73 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <doctest/doctest.h>
#include "mamba/specs/chimera_string_spec.hpp"
using namespace mamba::specs;
TEST_SUITE("specs::chimera_string_spec")
{
TEST_CASE("Free")
{
auto spec = ChimeraStringSpec();
CHECK(spec.contains(""));
CHECK(spec.contains("hello"));
CHECK_EQ(spec.str(), "*");
CHECK(spec.is_explicitly_free());
CHECK_FALSE(spec.is_exact());
CHECK(spec.is_glob());
}
TEST_CASE("mkl")
{
auto spec = ChimeraStringSpec::parse("mkl").value();
CHECK(spec.contains("mkl"));
CHECK_FALSE(spec.contains(""));
CHECK_FALSE(spec.contains("nomkl"));
CHECK_FALSE(spec.contains("hello"));
CHECK_EQ(spec.str(), "mkl");
CHECK_FALSE(spec.is_explicitly_free());
CHECK(spec.is_exact());
CHECK(spec.is_glob());
}
TEST_CASE("py.*")
{
auto spec = ChimeraStringSpec::parse("py.*").value();
CHECK(spec.contains("python"));
CHECK(spec.contains("py"));
CHECK(spec.contains("pypy"));
CHECK_FALSE(spec.contains(""));
CHECK_FALSE(spec.contains("cpython"));
CHECK_EQ(spec.str(), "^py.*$");
CHECK_FALSE(spec.is_explicitly_free());
CHECK_FALSE(spec.is_exact());
CHECK_FALSE(spec.is_glob());
}
TEST_CASE("^.*(accelerate|mkl)$")
{
auto spec = ChimeraStringSpec::parse("^.*(accelerate|mkl)$").value();
CHECK(spec.contains("accelerate"));
CHECK(spec.contains("mkl"));
CHECK_FALSE(spec.contains(""));
CHECK_FALSE(spec.contains("openblas"));
CHECK_EQ(spec.str(), "^.*(accelerate|mkl)$");
CHECK_FALSE(spec.is_explicitly_free());
CHECK_FALSE(spec.is_exact());
CHECK_FALSE(spec.is_glob());
}
}

View File

@ -24,7 +24,7 @@ TEST_SUITE("specs::match_spec")
auto ms = MatchSpec::parse("").value();
CHECK(ms.name().is_free());
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK(ms.build_number().is_explicitly_free());
CHECK_EQ(ms.str(), "*");
}
@ -325,7 +325,7 @@ TEST_SUITE("specs::match_spec")
auto ms = MatchSpec::parse("libblas=0.15*").value();
CHECK_EQ(ms.name().str(), "libblas");
CHECK_EQ(ms.version().str(), "=0.15*");
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "libblas=0.15*");
CHECK_EQ(ms.conda_build_form(), "libblas 0.15*.*");
}
@ -336,7 +336,7 @@ TEST_SUITE("specs::match_spec")
auto ms = MatchSpec::parse("xtensor =0.15*").value();
CHECK_EQ(ms.name().str(), "xtensor");
CHECK_EQ(ms.version().str(), "=0.15*");
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "xtensor=0.15*");
CHECK_EQ(ms.conda_build_form(), "xtensor 0.15*.*");
}
@ -346,7 +346,7 @@ TEST_SUITE("specs::match_spec")
auto ms = MatchSpec::parse("numpy=1.20").value();
CHECK_EQ(ms.name().str(), "numpy");
CHECK_EQ(ms.version().str(), "=1.20");
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "numpy=1.20");
}
@ -356,7 +356,7 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.channel().value().str(), "conda-forge");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "conda-forge::tzdata");
}
@ -366,7 +366,7 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.channel().value().str(), "conda-forge[noarch]");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "conda-forge[noarch]::tzdata");
}
@ -376,7 +376,7 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.channel().value().str(), "conda-forge[noarch]");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "conda-forge[noarch]::tzdata");
}
@ -386,7 +386,7 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.channel().value().str(), "pkgs/main");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "pkgs/main::tzdata");
}
@ -396,7 +396,7 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.channel().value().str(), "pkgs/main[noarch]");
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "pkgs/main[noarch]::tzdata");
}
@ -407,7 +407,7 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.platforms().value().get(), MatchSpec::platform_set{ "noarch" });
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "conda-forge[noarch]::tzdata");
}
@ -418,7 +418,7 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.platforms().value().get(), MatchSpec::platform_set{ "mamba-37" });
CHECK_EQ(ms.name().str(), "tzdata");
CHECK(ms.version().is_explicitly_free());
CHECK(ms.build_string().is_free());
CHECK(ms.build_string().is_explicitly_free());
CHECK_EQ(ms.str(), "conda-forge[mamba-37]::tzdata");
}
@ -435,6 +435,14 @@ TEST_SUITE("specs::match_spec")
CHECK_EQ(ms.build_string().str(), "py36h4a561cd_0");
CHECK_EQ(ms.str(), "conda-canary[linux-64]::conda==4.3.21.0post699+1dab973=py36h4a561cd_0");
}
SUBCASE("libblas[build=^.*(accelerate|mkl)$]")
{
auto ms = MatchSpec::parse("libblas[build=^.*(accelerate|mkl)$]").value();
CHECK_EQ(ms.name().str(), "libblas");
CHECK_EQ(ms.build_string().str(), "^.*(accelerate|mkl)$");
CHECK_FALSE(ms.build_string().is_glob());
}
}
TEST_CASE("Conda discrepencies")

View File

@ -0,0 +1,69 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <doctest/doctest.h>
#include "mamba/specs/regex_spec.hpp"
using namespace mamba::specs;
TEST_SUITE("specs::regex_spec")
{
TEST_CASE("Free")
{
auto spec = RegexSpec();
CHECK(spec.contains(""));
CHECK(spec.contains("hello"));
CHECK_EQ(spec.str(), "^.*$");
CHECK(spec.is_explicitly_free());
CHECK_FALSE(spec.is_exact());
}
TEST_CASE("mkl")
{
auto spec = RegexSpec::parse("mkl").value();
CHECK(spec.contains("mkl"));
CHECK_FALSE(spec.contains(""));
CHECK_FALSE(spec.contains("nomkl"));
CHECK_FALSE(spec.contains("hello"));
CHECK_EQ(spec.str(), "^mkl$");
CHECK_FALSE(spec.is_explicitly_free());
CHECK(spec.is_exact());
}
TEST_CASE("py.*")
{
auto spec = RegexSpec::parse("py.*").value();
CHECK(spec.contains("python"));
CHECK(spec.contains("py"));
CHECK(spec.contains("pypy"));
CHECK_FALSE(spec.contains(""));
CHECK_FALSE(spec.contains("cpython"));
CHECK_EQ(spec.str(), "^py.*$");
CHECK_FALSE(spec.is_explicitly_free());
CHECK_FALSE(spec.is_exact());
}
TEST_CASE("^.*(accelerate|mkl)$")
{
auto spec = RegexSpec::parse("^.*(accelerate|mkl)$").value();
CHECK(spec.contains("accelerate"));
CHECK(spec.contains("mkl"));
CHECK_FALSE(spec.contains(""));
CHECK_FALSE(spec.contains("openblas"));
CHECK_EQ(spec.str(), "^.*(accelerate|mkl)$");
CHECK_FALSE(spec.is_explicitly_free());
CHECK_FALSE(spec.is_exact());
}
}

View File

@ -715,9 +715,33 @@ namespace mambapy
.def("__copy__", &copy<GlobSpec>)
.def("__deepcopy__", &deepcopy<GlobSpec>, py::arg("memo"));
py::class_<RegexSpec>(m, "RegexSpec")
.def_readonly_static("free_pattern", &RegexSpec::free_pattern)
.def_readonly_static("pattern_start", &RegexSpec::pattern_start)
.def_readonly_static("pattern_end", &RegexSpec::pattern_end)
.def_static("parse", &RegexSpec::parse)
.def(py::init<>())
.def("contains", &RegexSpec::contains)
.def("is_explicitly_free", &RegexSpec::is_explicitly_free)
.def("is_exact", &RegexSpec::is_exact)
.def("__str__", &RegexSpec::str)
.def("__copy__", &copy<RegexSpec>)
.def("__deepcopy__", &deepcopy<RegexSpec>, py::arg("memo"));
py::class_<ChimeraStringSpec>(m, "ChimeraStringSpec")
.def_static("parse", &ChimeraStringSpec::parse)
.def(py::init<>())
.def("contains", &ChimeraStringSpec::contains)
.def("is_explicitly_free", &ChimeraStringSpec::is_explicitly_free)
.def("is_exact", &ChimeraStringSpec::is_exact)
.def("is_glob", &ChimeraStringSpec::is_glob)
.def("__str__", &ChimeraStringSpec::str)
.def("__copy__", &copy<ChimeraStringSpec>)
.def("__deepcopy__", &deepcopy<ChimeraStringSpec>, py::arg("memo"));
py::class_<MatchSpec>(m, "MatchSpec")
.def_property_readonly_static("NameSpec", &py::type::of<GlobSpec>)
.def_property_readonly_static("BuildStringSpec", &py::type::of<GlobSpec>)
.def_property_readonly_static("NameSpec", &py::type::of<MatchSpec::NameSpec>)
.def_property_readonly_static("BuildStringSpec", &py::type::of<MatchSpec::BuildStringSpec>)
.def_readonly_static("url_md5_sep", &MatchSpec::url_md5_sep)
.def_readonly_static("prefered_list_open", &MatchSpec::prefered_list_open)
.def_readonly_static("prefered_list_close", &MatchSpec::prefered_list_close)

View File

@ -812,7 +812,7 @@ def test_PackageInfo_V2Migrator():
def test_GlobSpec():
GlobSpec = libmambapy.specs.GlobSpec
spec = libmambapy.specs.GlobSpec("py*")
spec = GlobSpec("py*")
assert GlobSpec().is_free()
assert not spec.is_free()
@ -830,6 +830,49 @@ def test_GlobSpec():
assert other is not spec
def test_RegexSpec():
RegexSpec = libmambapy.specs.RegexSpec
spec = RegexSpec.parse("^py.*$")
assert RegexSpec().is_explicitly_free()
assert not spec.is_explicitly_free()
assert RegexSpec.parse("python").is_exact()
assert not spec.is_exact()
assert spec.contains("python")
assert str(spec) == "^py.*$"
# Copy
other = copy.deepcopy(spec)
assert str(other) == str(spec)
assert other is not spec
def test_ChimeraStringSpec():
ChimeraStringSpec = libmambapy.specs.ChimeraStringSpec
spec = ChimeraStringSpec.parse("^py.*$")
assert ChimeraStringSpec().is_explicitly_free()
assert not spec.is_explicitly_free()
assert ChimeraStringSpec().is_glob()
assert not spec.is_glob()
assert ChimeraStringSpec.parse("python").is_exact()
assert not spec.is_exact()
assert spec.contains("python")
assert str(spec) == "^py.*$"
# Copy
other = copy.deepcopy(spec)
assert str(other) == str(spec)
assert other is not spec
def test_MatchSpec():
MatchSpec = libmambapy.specs.MatchSpec