Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MatchSpec::contains_except_channel" #3231

Merged
merged 4 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/source/usage/specs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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"
60 changes: 60 additions & 0 deletions libmamba/include/mamba/specs/match_spec.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

namespace mamba::specs
{
class PackageInfo;

class MatchSpec
{
public:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
34 changes: 34 additions & 0 deletions libmamba/src/specs/match_spec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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())
Expand Down
226 changes: 226 additions & 0 deletions libmamba/tests/src/specs/test_match_spec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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" },
}));
}
}
}
Loading
Loading