diff --git a/libmambapy/src/libmambapy/bindings/legacy.cpp b/libmambapy/src/libmambapy/bindings/legacy.cpp index 4f68869596..b245a6a4e3 100644 --- a/libmambapy/src/libmambapy/bindings/legacy.cpp +++ b/libmambapy/src/libmambapy/bindings/legacy.cpp @@ -31,8 +31,6 @@ #include "mamba/core/transaction.hpp" #include "mamba/core/util_os.hpp" #include "mamba/core/virtual_packages.hpp" -#include "mamba/solver/libsolv/database.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" #include "mamba/solver/problems_graph.hpp" #include "mamba/validation/tools.hpp" #include "mamba/validation/update_framework_v0_6.hpp" @@ -247,10 +245,28 @@ bind_submodule_impl(pybind11::module_ m) throw std::runtime_error( // "Use Pool.add_repo_from_repodata_json or Pool.add_repo_from_native_serialization" " instead and cache with Pool.native_serialize_repo." - " Also consider load_subdir_in_pool for a high_level function to load" - " subdir index and manage cache, and load_installed_packages_in_pool for loading" - " prefix packages." - "The Repo class itself has been moved to libmambapy.solver.libsolv.RepoInfo." + " Also consider load_subdir_in_database for a high_level function to load" + " subdir index and manage cache, and load_installed_packages_in_database for" + " loading prefix packages." + " The Repo class itself has been moved to libmambapy.solver.libsolv.RepoInfo." + ); + } + )); + + struct PoolV2Migrator + { + }; + + py::class_(m, "Pool").def(py::init( + [](py::args, py::kwargs) -> PoolV2Migrator + { + throw std::runtime_error( // + "libmambapy.Pool has been moved to libmambapy.solver.libsolv.Database." + " The database contains functions to directly load packages, from a list or a" + " repodata.json." + " High level functions such as libmambapy.load_subdir_in_database and" + " libmambapy.load_installed_packages_in_database are also available to work" + " with other Mamba objects and Context parameters." ); } )); @@ -380,76 +396,19 @@ bind_submodule_impl(pybind11::module_ m) py::add_ostream_redirect(m, "ostream_redirect"); - py::class_(m, "Pool") - .def(py::init(), py::arg("channel_params")) - .def( - "set_logger", - &solver::libsolv::Database::set_logger, - py::call_guard() - ) - .def( - "add_repo_from_repodata_json", - &solver::libsolv::Database::add_repo_from_repodata_json, - py::arg("path"), - py::arg("url"), - py::arg("add_pip_as_python_dependency") = solver::libsolv::PipAsPythonDependency::No, - py::arg("use_only_tar_bz2") = solver::libsolv::UseOnlyTarBz2::No, - py::arg("repodata_parsers") = solver::libsolv::RepodataParser::Mamba - ) - .def( - "add_repo_from_native_serialization", - &solver::libsolv::Database::add_repo_from_native_serialization, - py::arg("path"), - py::arg("expected"), - py::arg("add_pip_as_python_dependency") = solver::libsolv::PipAsPythonDependency::No - ) - .def( - "add_repo_from_packages", - [](solver::libsolv::Database& db, - py::iterable packages, - std::string_view name, - solver::libsolv::PipAsPythonDependency add) - { - // TODO(C++20): No need to copy in a vector, simply transform the input range. - auto pkg_infos = std::vector(); - for (py::handle pkg : packages) - { - pkg_infos.push_back(pkg.cast()); - } - return db.add_repo_from_packages(pkg_infos, name, add); - }, - py::arg("packages"), - py::arg("name") = "", - py::arg("add_pip_as_python_dependency") = solver::libsolv::PipAsPythonDependency::No - ) - .def( - "native_serialize_repo", - &solver::libsolv::Database::native_serialize_repo, - py::arg("repo"), - py::arg("path"), - py::arg("metadata") - ) - .def("set_installed_repo", &solver::libsolv::Database::set_installed_repo, py::arg("repo")) - .def( - "set_repo_priority", - &solver::libsolv::Database::set_repo_priority, - py::arg("repo"), - py::arg("priorities") - ); - m.def( - "load_subdir_in_pool", + "load_subdir_in_database", &load_subdir_in_database, py::arg("context"), - py::arg("pool"), + py::arg("database"), py::arg("subdir") ); m.def( - "load_installed_packages_in_pool", + "load_installed_packages_in_database", &load_installed_packages_in_database, py::arg("context"), - py::arg("pool"), + py::arg("database"), py::arg("prefix_data") ); @@ -534,7 +493,7 @@ bind_submodule_impl(pybind11::module_ m) "create_repo", [](SubdirData& subdir, solver::libsolv::Database& db) -> solver::libsolv::RepoInfo { - deprecated("Use `load_subdir_in_pool` instead", "2.0"); + deprecated("Use libmambapy.load_subdir_in_database instead", "2.0"); return extract(load_subdir_in_database(mambapy::singletons.context(), db, subdir)); } ) diff --git a/libmambapy/src/libmambapy/bindings/solver_libsolv.cpp b/libmambapy/src/libmambapy/bindings/solver_libsolv.cpp index 4d0b86cbbf..cac09624b6 100644 --- a/libmambapy/src/libmambapy/bindings/solver_libsolv.cpp +++ b/libmambapy/src/libmambapy/bindings/solver_libsolv.cpp @@ -8,6 +8,7 @@ #include #include "mamba/core/palette.hpp" +#include "mamba/fs/filesystem.hpp" #include "mamba/solver/libsolv/database.hpp" #include "mamba/solver/libsolv/parameters.hpp" #include "mamba/solver/libsolv/repo_info.hpp" @@ -98,15 +99,109 @@ namespace mambapy .def("__deepcopy__", &deepcopy, py::arg("memo")); py::class_(m, "RepoInfo") - .def("id", &RepoInfo::id) - .def("name", &RepoInfo::name) - .def("priority", &RepoInfo::priority) + .def_property_readonly("id", &RepoInfo::id) + .def_property_readonly("name", &RepoInfo::name) + .def_property_readonly("priority", &RepoInfo::priority) .def("package_count", &RepoInfo::package_count) .def(py::self == py::self) .def(py::self != py::self) .def("__copy__", ©) .def("__deepcopy__", &deepcopy, py::arg("memo")); + py::class_(m, "Database") + .def(py::init(), py::arg("channel_params")) + .def("set_logger", &Database::set_logger, py::call_guard()) + .def( + "add_repo_from_repodata_json", + &Database::add_repo_from_repodata_json, + py::arg("path"), + py::arg("url"), + py::arg("add_pip_as_python_dependency") = PipAsPythonDependency::No, + py::arg("use_only_tar_bz2") = UseOnlyTarBz2::No, + py::arg("repodata_parser") = RepodataParser::Mamba + ) + .def( + "add_repo_from_native_serialization", + &Database::add_repo_from_native_serialization, + py::arg("path"), + py::arg("expected"), + py::arg("add_pip_as_python_dependency") = PipAsPythonDependency::No + ) + .def( + "add_repo_from_packages", + [](Database& db, py::iterable packages, std::string_view name, PipAsPythonDependency add) + { + // TODO(C++20): No need to copy in a vector, simply transform the input range. + auto pkg_infos = std::vector(); + for (py::handle pkg : packages) + { + pkg_infos.push_back(pkg.cast()); + } + return db.add_repo_from_packages(pkg_infos, name, add); + }, + py::arg("packages"), + py::arg("name") = "", + py::arg("add_pip_as_python_dependency") = PipAsPythonDependency::No + ) + .def( + "native_serialize_repo", + &Database::native_serialize_repo, + py::arg("repo"), + py::arg("path"), + py::arg("metadata") + ) + .def("set_installed_repo", &Database::set_installed_repo, py::arg("repo")) + .def("installed_repo", &Database::installed_repo) + .def("set_repo_priority", &Database::set_repo_priority, py::arg("repo"), py::arg("priorities")) + .def("remove_repo", &Database::remove_repo, py::arg("repo")) + .def("repo_count", &Database::repo_count) + .def("package_count", &Database::package_count) + .def( + "packages_in_repo", + [](const Database& db, RepoInfo repo) + { + // TODO(C++20): When Database function are refactored to use range, take the + // opportunity here to make a Python iterator to avoid large alloc. + auto out = py::list(); + db.for_each_package_in_repo( + repo, + [&](specs::PackageInfo&& pkg) { out.append(std::move(pkg)); } + ); + return out; + }, + py::arg("repo") + ) + .def( + "packages_matching", + [](Database& db, const specs::MatchSpec& ms) + { + // TODO(C++20): When Database function are refactored to use range, take the + // opportunity here to make a Python iterator to avoid large alloc. + auto out = py::list(); + db.for_each_package_matching( + ms, + [&](specs::PackageInfo&& pkg) { out.append(std::move(pkg)); } + ); + return out; + }, + py::arg("spec") + ) + .def( + "packages_depending_on", + [](Database& db, const specs::MatchSpec& ms) + { + // TODO(C++20): When Database function are refactored to use range, take the + // opportunity here to make a Python iterator to avoid large alloc. + auto out = py::list(); + db.for_each_package_depending_on( + ms, + [&](specs::PackageInfo&& pkg) { out.append(std::move(pkg)); } + ); + return out; + }, + py::arg("spec") + ); + py::class_(m, "UnSolvable") .def("problems", &UnSolvable::problems) .def("problems_to_str", &UnSolvable::problems_to_str) diff --git a/libmambapy/tests/test_solver_libsolv.py b/libmambapy/tests/test_solver_libsolv.py index 714045692e..615614646c 100644 --- a/libmambapy/tests/test_solver_libsolv.py +++ b/libmambapy/tests/test_solver_libsolv.py @@ -1,7 +1,10 @@ import copy +import json +import itertools import pytest +import libmambapy import libmambapy.solver.libsolv as libsolv @@ -103,3 +106,95 @@ def test_RepodataOrigin(): other = copy.deepcopy(orig) assert other is not orig assert other == orig + + +@pytest.mark.parametrize("add_pip_as_python_dependency", [True, False]) +def test_Database_RepoInfo_from_packages(add_pip_as_python_dependency): + db = libsolv.Database(libmambapy.specs.ChannelResolveParams()) + assert db.repo_count() == 0 + assert db.installed_repo() is None + assert db.package_count() == 0 + + repo = db.add_repo_from_packages( + [libmambapy.specs.PackageInfo(name="python")], + name="duck", + add_pip_as_python_dependency=add_pip_as_python_dependency, + ) + db.set_installed_repo(repo) + + assert repo.id > 0 + assert repo.name == "duck" + assert repo.priority == libsolv.Priorities() + assert repo.package_count() == 1 + assert db.repo_count() == 1 + assert db.package_count() == 1 + assert db.installed_repo() == repo + + new_priority = libsolv.Priorities(2, 3) + db.set_repo_priority(repo, new_priority) + assert repo.priority == new_priority + + pkgs = db.packages_in_repo(repo) + assert len(pkgs) == 1 + assert pkgs[0].name == "python" + assert pkgs[0].depends == [] if add_pip_as_python_dependency else ["pip"] + + db.remove_repo(repo) + assert db.repo_count() == 0 + assert db.package_count() == 0 + assert db.installed_repo() is None + + +@pytest.fixture +def tmp_repodata_json(tmp_path): + file = tmp_path / "repodata.json" + with open(file, "w+") as f: + json.dump( + { + "packages": { + "python-1.0-bld": { + "name": "python", + "version": "1.0", + "build": "bld", + "build_number": 0, + }, + }, + "packages.conda": { + "foo-1.0-bld": { + "name": "foo", + "version": "1.0", + "build": "bld", + "build_number": 0, + } + }, + }, + f, + ) + return file + + +@pytest.mark.parametrize( + ["add_pip_as_python_dependency", "use_only_tar_bz2", "repodata_parser"], + itertools.product([True, False], [True, False], ["Mamba", "Libsolv"]), +) +def test_Database_RepoInfo_from_repodata( + tmp_repodata_json, add_pip_as_python_dependency, use_only_tar_bz2, repodata_parser +): + db = libsolv.Database(libmambapy.specs.ChannelResolveParams()) + + repo = db.add_repo_from_repodata_json( + path=str(tmp_repodata_json), + url="https://repo.mamba.pm", + add_pip_as_python_dependency=add_pip_as_python_dependency, + use_only_tar_bz2=use_only_tar_bz2, + repodata_parser=repodata_parser, + ) + db.set_installed_repo(repo) + + assert repo.package_count() == 1 if use_only_tar_bz2 else 2 + assert db.package_count() == repo.package_count() + + pkgs = db.packages_in_repo(repo) + assert len(pkgs) == repo.package_count() + assert pkgs[0].name == "python" + assert pkgs[0].depends == [] if add_pip_as_python_dependency else ["pip"]