Skip to content

Commit

Permalink
Add MatchSpec::contains_except_channel" (#3231)
Browse files Browse the repository at this point in the history
* Add MatchSpec::contains_except_channel

* Add MatchSpec::contains_except_channel overload

* Bind MatchSpec::contains_except_channel

* Add documentation for MatchSpec::contains
  • Loading branch information
AntoinePrv authored Mar 13, 2024
1 parent ce840bb commit 7125518
Show file tree
Hide file tree
Showing 6 changed files with 426 additions and 18 deletions.
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

0 comments on commit 7125518

Please sign in to comment.