Add MatchSpec::contains_except_channel" (#3231)

* Add MatchSpec::contains_except_channel

* Add MatchSpec::contains_except_channel overload

* Bind MatchSpec::contains_except_channel

* Add documentation for MatchSpec::contains
This commit is contained in:
Antoine Prouvost 2024-03-13 16:37:25 +01:00 committed by GitHub
parent ce840bbf8c
commit 7125518164
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 426 additions and 18 deletions

View File

@ -431,3 +431,24 @@ with an example
``python==3.7`` (strong equality).
This is intuitively different from how we write ``python=3.7``, which we must write with
attributes as ``python[version="=3.7"]``.
The method
:cpp:func:`MatchSpec.contains_except_channel <mamba::specs::MatchSpec::contains_except_channel>`
can be used to check if a package is contained (matched) by the current |MatchSpec|.
The somewhat verbose name serve to indicate that the channel is ignored in this function.
As mentionned in the :ref:`Channel section<libmamba_usage_channel>` resolving and matching channels
is a delicate operation.
In addition, the channel is a part that describe the **provenance** of a package and not is content
so various application ay want to handle it in different ways.
The :cpp:func:`MatchSpec.channel <mamba::specs::MatchSpec::channel>` attribute can be used to
reason about the possible channel contained in the |MatchSpec|.
.. code:: python
import libmambapy.specs as specs
ms = specs.MatchSpec.parse("conda-forge::py*[build_number='>4']")
assert ms.contains(name="python", build_number=5)
assert not ms.contains(name="numpy", build_number=8)
assert ms.channel.location == "conda-forge"

View File

@ -24,6 +24,8 @@
namespace mamba::specs
{
class PackageInfo;
class MatchSpec
{
public:
@ -105,6 +107,25 @@ namespace mamba::specs
[[nodiscard]] auto is_simple() const -> bool;
/**
* Check if the MatchSpec matches the given package.
*
* The check exclude anything related to the channel, du to the difficulties in
* comparing unresolved channels and the fact that this check can be also be done once
* at a repository level when the user knows how packages are organised.
*
* This function is written as a generic template, to acomodate various uses: the fact
* that the attributes may not always be in the correct format in the package, and that
* their parsing may be cached.
*/
template <typename Pkg>
[[nodiscard]] auto contains_except_channel(const Pkg& pkg) const -> bool;
/**
* Convenience wrapper making necessary convertions.
*/
[[nodiscard]] auto contains_except_channel(const PackageInfo& pkg) const -> bool;
private:
struct ExtraMembers
@ -156,4 +177,43 @@ struct fmt::formatter<::mamba::specs::MatchSpec>
auto format(const ::mamba::specs::MatchSpec& spec, format_context& ctx) -> decltype(ctx.out());
};
/*********************************
* Implementation of MatchSpec *
*********************************/
namespace mamba::specs
{
template <typename Pkg>
auto MatchSpec::contains_except_channel(const Pkg& pkg) const -> bool
{
if ( //
!name().contains(std::invoke(&Pkg::name, pkg)) //
|| !version().contains(std::invoke(&Pkg::version, pkg)) //
|| !build_string().contains(std::invoke(&Pkg::build_string, pkg)) //
|| !build_number().contains(std::invoke(&Pkg::build_number, pkg)) //
|| (!md5().empty() && (md5() != std::invoke(&Pkg::md5, pkg))) //
|| (!sha256().empty() && (sha256() != std::invoke(&Pkg::sha256, pkg))) //
|| (!license().empty() && (license() != std::invoke(&Pkg::license, pkg))) //
)
{
return false;
}
if (const auto& plats = platforms();
plats.has_value() && !plats->get().contains(std::invoke(&Pkg::platform, pkg)))
{
return false;
}
if (const auto& tfeats = track_features();
tfeats.has_value()
&& !util::set_is_subset_of(tfeats->get(), std::invoke(&Pkg::track_features, pkg)))
{
return false;
}
return true;
}
}
#endif

View File

@ -12,6 +12,7 @@
#include "mamba/specs/archive.hpp"
#include "mamba/specs/match_spec.hpp"
#include "mamba/specs/package_info.hpp"
#include "mamba/util/parsers.hpp"
#include "mamba/util/string.hpp"
@ -944,6 +945,39 @@ namespace mamba::specs
&& m_build_number.is_explicitly_free();
}
auto MatchSpec::contains_except_channel(const PackageInfo& pkg) const -> bool
{
struct Pkg
{
std::string_view name;
Version version; // Converted
std::string_view build_string;
std::size_t build_number;
std::string_view md5;
std::string_view sha256;
std::string_view license;
std::reference_wrapper<const std::string> platform;
string_set track_features; // Converted
};
auto maybe_ver = Version::parse(pkg.version.empty() ? "0" : pkg.version);
if (!maybe_ver)
{
return false;
}
return contains_except_channel(Pkg{
/* .name= */ pkg.name,
/* .version= */ std::move(maybe_ver).value(),
/* .build_string= */ pkg.build_string,
/* .build_number= */ pkg.build_number,
/* .md5= */ pkg.md5,
/* .sha256= */ pkg.sha256,
/* .license= */ pkg.license,
/* .platform= */ pkg.platform,
/* .track_features= */ string_set(pkg.track_features.cbegin(), pkg.track_features.cend()),
});
}
auto MatchSpec::extra() -> ExtraMembers&
{
if (!m_extra.has_value())

View File

@ -7,6 +7,7 @@
#include <doctest/doctest.h>
#include "mamba/specs/match_spec.hpp"
#include "mamba/specs/package_info.hpp"
#include "mamba/util/string.hpp"
using namespace mamba;
@ -496,4 +497,229 @@ TEST_SUITE("specs::match_spec")
CHECK_FALSE(ms.is_simple());
}
}
TEST_CASE("MatchSpec::contains")
{
// Note that tests for individual ``contains`` functions (``VersionSpec::contains``,
// ``BuildNumber::contains``, ``GlobSpec::contains``...) are tested in their respective
// test files.
using namespace specs::match_spec_literals;
using namespace specs::version_literals;
struct Pkg
{
std::string name = {};
specs::Version version = {};
std::string build_string = {};
std::size_t build_number = {};
std::string md5 = {};
std::string sha256 = {};
std::string license = {};
DynamicPlatform platform = {};
MatchSpec::string_set track_features = {};
};
SUBCASE("python")
{
const auto ms = "python"_ms;
CHECK(ms.contains_except_channel(Pkg{ "python" }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "pypy" }));
CHECK(ms.contains_except_channel(PackageInfo{ "python" }));
CHECK_FALSE(ms.contains_except_channel(PackageInfo{ "pypy" }));
}
SUBCASE("py*")
{
const auto ms = "py*"_ms;
CHECK(ms.contains_except_channel(Pkg{ "python" }));
CHECK(ms.contains_except_channel(Pkg{ "pypy" }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "rust" }));
CHECK(ms.contains_except_channel(PackageInfo{ "python" }));
CHECK(ms.contains_except_channel(PackageInfo{ "pypy" }));
CHECK_FALSE(ms.contains_except_channel(PackageInfo{ "rust" }));
}
SUBCASE("py*>=3.7")
{
const auto ms = "py*>=3.7"_ms;
CHECK(ms.contains_except_channel(Pkg{ "python", "3.7"_v }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "pypy", "3.6"_v }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "rust", "3.7"_v }));
CHECK(ms.contains_except_channel(PackageInfo{ "python", "3.7", "bld", 0 }));
CHECK_FALSE(ms.contains_except_channel(PackageInfo{ "pypy", "3.6", "bld", 0 }));
CHECK_FALSE(ms.contains_except_channel(PackageInfo{ "rust", "3.7", "bld", 0 }));
}
SUBCASE("py*>=3.7=*cpython")
{
const auto ms = "py*>=3.7=*cpython"_ms;
CHECK(ms.contains_except_channel(Pkg{ "python", "3.7"_v, "37_cpython" }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "pypy", "3.6"_v, "cpython" }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "pypy", "3.8"_v, "pypy" }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "rust", "3.7"_v, "cpyhton" }));
}
SUBCASE("py*[version='>=3.7', build=*cpython]")
{
const auto ms = "py*[version='>=3.7', build=*cpython]"_ms;
CHECK(ms.contains_except_channel(Pkg{ "python", "3.7"_v, "37_cpython" }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "pypy", "3.6"_v, "cpython" }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "pypy", "3.8"_v, "pypy" }));
CHECK_FALSE(ms.contains_except_channel(Pkg{ "rust", "3.7"_v, "cpyhton" }));
}
SUBCASE("pkg[build_number='>3']")
{
const auto ms = "pkg[build_number='>3']"_ms;
auto pkg = Pkg{ "pkg" };
pkg.build_number = 4;
CHECK(ms.contains_except_channel(pkg));
pkg.build_number = 2;
CHECK_FALSE(ms.contains_except_channel(pkg));
}
SUBCASE("pkg[md5=helloiamnotreallymd5haha]")
{
const auto ms = "pkg[md5=helloiamnotreallymd5haha]"_ms;
auto pkg = Pkg{ "pkg" };
pkg.md5 = "helloiamnotreallymd5haha";
CHECK(ms.contains_except_channel(pkg));
for (auto md5 : { "helloiamnotreallymd5hahaevillaugh", "hello", "" })
{
CAPTURE(std::string_view(md5));
pkg.md5 = md5;
CHECK_FALSE(ms.contains_except_channel(pkg));
}
}
SUBCASE("pkg[sha256=helloiamnotreallysha256hihi]")
{
const auto ms = "pkg[sha256=helloiamnotreallysha256hihi]"_ms;
auto pkg = Pkg{ "pkg" };
pkg.sha256 = "helloiamnotreallysha256hihi";
CHECK(ms.contains_except_channel(pkg));
for (auto sha256 : { "helloiamnotreallysha256hihicutelaugh", "hello", "" })
{
CAPTURE(std::string_view(sha256));
pkg.sha256 = sha256;
CHECK_FALSE(ms.contains_except_channel(pkg));
}
}
SUBCASE("pkg[license=helloiamnotreallylicensehoho]")
{
const auto ms = "pkg[license=helloiamnotreallylicensehoho]"_ms;
auto pkg = Pkg{ "pkg" };
pkg.license = "helloiamnotreallylicensehoho";
CHECK(ms.contains_except_channel(pkg));
for (auto license : { "helloiamnotreallylicensehohodadlaugh", "hello", "" })
{
CAPTURE(std::string_view(license));
pkg.license = license;
CHECK_FALSE(ms.contains_except_channel(pkg));
}
}
SUBCASE("pkg[subdir='linux-64,linux-64-512']")
{
const auto ms = "pkg[subdir='linux-64,linux-64-512']"_ms;
auto pkg = Pkg{ "pkg" };
for (auto plat : { "linux-64", "linux-64-512" })
{
CAPTURE(std::string_view(plat));
pkg.platform = plat;
CHECK(ms.contains_except_channel(pkg));
}
for (auto plat : { "linux", "linux-512", "", "linux-64,linux-64-512" })
{
CAPTURE(std::string_view(plat));
pkg.platform = plat;
CHECK_FALSE(ms.contains_except_channel(pkg));
}
}
SUBCASE("pkg[track_features='mkl,openssl']")
{
using string_set = typename MatchSpec::string_set;
const auto ms = "pkg[track_features='mkl,openssl']"_ms;
auto pkg = Pkg{ "pkg" };
for (auto tfeats : { string_set{ "openssl", "mkl" } })
{
pkg.track_features = tfeats;
CHECK(ms.contains_except_channel(pkg));
}
for (auto tfeats : { string_set{ "openssl" }, string_set{ "mkl" }, string_set{} })
{
pkg.track_features = tfeats;
CHECK_FALSE(ms.contains_except_channel(pkg));
}
}
SUBCASE("Complex")
{
const auto ms = "py*>=3.7=bld[build_number='<=2', md5=lemd5, track_features='mkl,openssl']"_ms;
CHECK(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "3.8.0"_v,
/* .build_string= */ "bld",
/* .build_number= */ 2,
/* .md5= */ "lemd5",
/* .sha256= */ "somesha256",
/* .license= */ "MIT",
/* .platform= */ "linux-64",
/* .track_features =*/{ "openssl", "mkl" },
}));
CHECK(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "3.12.0"_v,
/* .build_string= */ "bld",
/* .build_number= */ 0,
/* .md5= */ "lemd5",
/* .sha256= */ "somesha256",
/* .license= */ "GPL",
/* .platform= */ "linux-64",
/* .track_features =*/{ "openssl", "mkl" },
}));
CHECK_FALSE(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "3.3.0"_v, // Not matching
/* .build_string= */ "bld",
/* .build_number= */ 0,
/* .md5= */ "lemd5",
/* .sha256= */ "somesha256",
/* .license= */ "GPL",
/* .platform= */ "linux-64",
/* .track_features =*/{ "openssl", "mkl" },
}));
CHECK_FALSE(ms.contains_except_channel(Pkg{
/* .name= */ "python",
/* .version= */ "3.12.0"_v,
/* .build_string= */ "bld",
/* .build_number= */ 0,
/* .md5= */ "wrong", // Not matching
/* .sha256= */ "somesha256",
/* .license= */ "GPL",
/* .platform= */ "linux-64",
/* .track_features =*/{ "openssl", "mkl" },
}));
}
}
}

View File

@ -306,9 +306,9 @@ namespace mambapy
py::arg("credentials") = CondaURL::Credentials::Hide
);
auto py_channel_spec = py::class_<UnresolvedChannel>(m, "UnresolvedChannel");
auto py_unresolved_channel = py::class_<UnresolvedChannel>(m, "UnresolvedChannel");
py::enum_<UnresolvedChannel::Type>(py_channel_spec, "Type")
py::enum_<UnresolvedChannel::Type>(py_unresolved_channel, "Type")
.value("URL", UnresolvedChannel::Type::URL)
.value("PackageURL", UnresolvedChannel::Type::PackageURL)
.value("Path", UnresolvedChannel::Type::Path)
@ -318,7 +318,7 @@ namespace mambapy
.def(py::init(&enum_from_str<UnresolvedChannel::Type>));
py::implicitly_convertible<py::str, UnresolvedChannel::Type>();
py_channel_spec //
py_unresolved_channel //
.def_static("parse", UnresolvedChannel::parse)
.def(
py::init<std::string, UnresolvedChannel::platform_set, UnresolvedChannel::Type>(),
@ -621,21 +621,21 @@ namespace mambapy
pkg.version = std::move(version);
pkg.build_string = std::move(build_string);
pkg.build_number = std::move(build_number);
pkg.channel = channel;
pkg.package_url = package_url;
pkg.platform = platform;
pkg.filename = filename;
pkg.license = license;
pkg.md5 = md5;
pkg.sha256 = sha256;
pkg.signatures = signatures;
pkg.track_features = track_features;
pkg.dependencies = depends;
pkg.constrains = constrains;
pkg.defaulted_keys = defaulted_keys;
pkg.noarch = noarch;
pkg.size = size;
pkg.timestamp = timestamp;
pkg.channel = std::move(channel);
pkg.package_url = std::move(package_url);
pkg.platform = std::move(platform);
pkg.filename = std::move(filename);
pkg.license = std::move(license);
pkg.md5 = std::move(md5);
pkg.sha256 = std::move(sha256);
pkg.signatures = std::move(signatures);
pkg.track_features = std::move(track_features);
pkg.dependencies = std::move(depends);
pkg.constrains = std::move(constrains);
pkg.defaulted_keys = std::move(defaulted_keys);
pkg.noarch = std::move(noarch);
pkg.size = std::move(size);
pkg.timestamp = std::move(timestamp);
return pkg;
}
),
@ -752,6 +752,59 @@ namespace mambapy
.def_property("features", &MatchSpec::features, &MatchSpec::set_features)
.def_property("track_features", &MatchSpec::track_features, &MatchSpec::set_track_features)
.def_property("optional", &MatchSpec::optional, &MatchSpec::set_optional)
.def(
"contains_except_channel",
[](const MatchSpec& ms, const PackageInfo& pkg)
{ return ms.contains_except_channel(pkg); }
)
.def(
"contains_except_channel",
[](const MatchSpec& ms,
std::string_view name,
const Version& version,
std::string_view build_string,
std::size_t build_number,
std::string_view md5,
std::string_view sha256,
std::string_view license,
std::string& platform,
MatchSpec::string_set track_features)
{
struct Pkg
{
std::string_view name;
std::reference_wrapper<const Version> version;
std::string_view build_string;
std::size_t build_number;
std::string_view md5;
std::string_view sha256;
std::string_view license;
std::reference_wrapper<const std::string> platform;
const MatchSpec::string_set track_features;
};
return ms.contains_except_channel(Pkg{
/* .name= */ name,
/* .version= */ version,
/* .build_string= */ build_string,
/* .build_number= */ build_number,
/* .md5= */ md5,
/* .sha256= */ sha256,
/* .license= */ license,
/* .platform= */ platform,
/* .track_features= */ std::move(track_features),
});
},
py::arg("name") = "",
py::arg("version") = Version(),
py::arg("build_string") = "",
py::arg("build_number") = 0,
py::arg("md5") = "",
py::arg("sha256") = "",
py::arg("license") = "",
py::arg("platform") = "",
py::arg("track_features") = MatchSpec::string_set{}
)
.def("is_file", &MatchSpec::is_file)
.def("is_simple", &MatchSpec::is_simple)
.def("conda_build_form", &MatchSpec::conda_build_form)

View File

@ -871,6 +871,20 @@ def test_MatchSpec():
assert other is not ms
def test_MatchSpec_contains():
MatchSpec = libmambapy.specs.MatchSpec
PackageInfo = libmambapy.specs.PackageInfo
ms = MatchSpec.parse("conda-forge::py*[build_number='>4']")
assert ms.contains_except_channel(name="python", build_number=5, build_string="bld")
assert not ms.contains_except_channel(name="python", build_number=2)
assert not ms.contains_except_channel(name="rust", build_number=4)
assert ms.contains_except_channel(PackageInfo(name="python", build_number=5))
assert not ms.contains_except_channel(PackageInfo(name="python"))
def test_MatchSpec_V2Migrator():
"""Explicit migration help added from v1 to v2."""
import libmambapy