mirror of https://github.com/mamba-org/mamba.git
Finalized Solver bindings and add solver doc (#3195)
* Fix cross reference in docs * Rename Request::Item > Job * Add missing Request python test * Add Solver usage documentation * Add tests for ProblemsGraph * Adjust 2.0 changes * use mambatest directories * Remove channel from test repodata.json * Add simple solver tests
This commit is contained in:
parent
3a6d010e38
commit
6d535ea5ff
|
@ -0,0 +1,9 @@
|
|||
``mamba::solver``
|
||||
=================
|
||||
|
||||
The ``mamba::solver`` namespace contains the packages solver and related objects.
|
||||
This particular namespace is a generic common interface, while the subnamespace
|
||||
``mamba::solver::libsolv`` contains `LibSolv <https://en.opensuse.org/openSUSE:Libzypp_satsolver>`_
|
||||
specific implementations.
|
||||
|
||||
.. doxygennamespace:: mamba::solver
|
|
@ -59,25 +59,40 @@ Changes inlcude:
|
|||
- The global ``Context``, previously available through ``Context()``, must now be accessed through
|
||||
``Context.instance()``.
|
||||
What's more, it is required to be passed explicitly in a few more functions.
|
||||
Future version of ``libmambapy`` will continue in this direction until there are no global context.In version 2, ``Context()`` will throw an exception to avoid hard to catch errors.
|
||||
Future version of ``libmambapy`` will continue in this direction until there are no global context.
|
||||
In version 2, ``Context()`` will throw an exception to avoid hard to catch errors.
|
||||
- ``ChannelContext`` is no longer an implicit global variable.
|
||||
It must be constructed with one of ``ChannelContext.make_simple`` or
|
||||
``ChannelContext.make_conda_compatible`` (with ``Context.instance`` as argument in most cases)
|
||||
and passed explicitly to a few functions.
|
||||
- ``Channel`` has been redesigned an moved to a new ``libmambapy.specs``.
|
||||
A featureful ``libmambapy.specs.CondaURL`` is used to describe channel URLs.
|
||||
This module also includes ``UnresolvedChannel`` used to describe unresolved channel strings.
|
||||
- ``MatchSpec`` has been redesigned and moved to ``libmambapy.specs``.
|
||||
The module also includes a platform enumeration, an implementation of ordered ``Version``, and a
|
||||
``VersionSpec`` to match versions.
|
||||
- ``PackageInfo`` has been moved to ``libmambapy.specs``.
|
||||
Some attributes have been given a more explicit name ``fn`` > ``filename``,
|
||||
``url`` > ``package_url``.
|
||||
- ``Repo`` has been redesigned into a lightweight ``RepoInfo`` and moved to
|
||||
``libmambapy.solver.libsolv``.
|
||||
The creation and modification of repos happens through the ``Pool``, with methods such as
|
||||
``Pool.add_repo_from_repodata_json`` and ``Pool.add_repo_from_packages``, but also high-level
|
||||
free functions such as ``load_subdir_in_pool`` and ``load_installed_packages_in_pool``.
|
||||
- A new ``Context`` independent submodule ``libmambapy.specs`` has been introduced with:
|
||||
- The redesign of the ``Channel`` and a new ``UnresolvedChannel`` used to describe unresolved
|
||||
channel strings.
|
||||
A featureful ``libmambapy.specs.CondaURL`` is used to describe channel URLs.
|
||||
- The redesign``MatchSpec``.
|
||||
The module also includes a platform enumeration, an implementation of ordered ``Version``,
|
||||
and a ``VersionSpec`` to match versions.
|
||||
- ``PackageInfo`` has been moved to this submodule.
|
||||
Some attributes have been given a more explicit name ``fn`` > ``filename``,
|
||||
``url`` > ``package_url``.
|
||||
- A new ``Context`` independent submodule ``libmambapy.solver`` has been introduced with the
|
||||
changes below.
|
||||
A usage documentation page is available at
|
||||
https://mamba.readthedocs.io/en/latest/usage/solver.html
|
||||
- The redesign of the ``Pool``, which is now available as ``libmambapy.solver.libsolv.Database``.
|
||||
The new interfaces makes it easier to create repositories without using other ``libmambapy``
|
||||
objects.
|
||||
- ``Repo`` has been redesigned into a lightweight ``RepoInfo`` and moved to
|
||||
``libmambapy.solver.libsolv``.
|
||||
The creation and modification of repos happens through the ``Database``, with methods such as
|
||||
``Database.add_repo_from_repodata_json`` and ``Database.add_repo_from_packages``, but also
|
||||
high-level free functions such as ``load_subdir_in_database`` and
|
||||
``load_installed_packages_in_database``.
|
||||
- The ``Solver`` has been moved to ``libmambapy.solver.libsolv.Solver``.
|
||||
- All jobs, pins, and flags must be passed as a single ``libmambapy.solver.Request``.
|
||||
- The outcome of solving the request is either a ``libmambapy.solver.Solution`` or a
|
||||
``libmambapy.solver.libsolv.Unsolvable`` state from which rich error messages can be
|
||||
extracted.
|
||||
|
||||
.. TODO include final decision for Channels as URLs.
|
||||
|
||||
|
@ -99,14 +114,19 @@ The main changes are:
|
|||
- A cleanup of ``ChannelContext`` for be a light proxy and parameter holder wrapping the
|
||||
``specs::Channel``.
|
||||
- A new ``repodata.json`` parser using `simdjson <https://simdjson.org/>`_.
|
||||
- [WIP] Creation of the ``solver::libsolv`` sub-namespace for full isolation of libsolv, and a
|
||||
- The ``MPool``, ``MRepo`` and ``MSolver`` API has been completely redesigned into a ``solver``
|
||||
subnamespace and works independently of the ``Context``.
|
||||
The ``solver::libsolv`` sub-namespace has also been added for full isolation of libsolv, and a
|
||||
solver API without ``Context``.
|
||||
It currently contains:
|
||||
The ``solver`` API redesign includes:
|
||||
- A refactoring of the ``MPool`` as a ``DataBase``, fully isolates libsolv, and simplifies
|
||||
repository creation.
|
||||
- A refactoring and thinning of ``MRepo`` as a new ``RepoInfo``.
|
||||
- A refactoring of ``Solver``, whose outcome is split between a ``Solution`` and an
|
||||
``UnSolvable`` state.
|
||||
- The ``ProblemsGraph`` reach SAT error state.
|
||||
|
||||
- A solver ``Request`` with all requirements to solve is the new way to specify jobs.
|
||||
- A refactoring of ``Solver``.
|
||||
- A solver outcome as either a ``Solution`` or an ``UnSolvable`` state.
|
||||
A usage documentation (in Python) is available at
|
||||
https://mamba.readthedocs.io/en/latest/usage/solver.html
|
||||
- Improved downloaders.
|
||||
|
||||
.. TODO OCI registry
|
||||
|
|
|
@ -57,6 +57,7 @@ You can try Mamba now by visiting the installation for
|
|||
|
||||
python_api
|
||||
usage/specs
|
||||
usage/solver
|
||||
|
||||
.. toctree::
|
||||
:caption: API REFERENCE
|
||||
|
@ -64,6 +65,7 @@ You can try Mamba now by visiting the installation for
|
|||
:hidden:
|
||||
|
||||
api/specs
|
||||
api/solver
|
||||
|
||||
.. toctree::
|
||||
:caption: DEVELOPER ZONE
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
Solving Package Environments
|
||||
============================
|
||||
|
||||
.. |MatchSpec| replace:: :cpp:type:`MatchSpec <mamba::specs::MatchSpec>`
|
||||
.. |PackageInfo| replace:: :cpp:type:`PackageInfo <mamba::specs::PackageInfo>`
|
||||
.. |Database| replace:: :cpp:type:`Database <mamba::solver::libsolv::Database>`
|
||||
.. |Request| replace:: :cpp:type:`Request <mamba::solver::Request>`
|
||||
.. |Solver| replace:: :cpp:type:`Solver <mamba::solver::libsolv::Solver>`
|
||||
.. |Solution| replace:: :cpp:type:`Solution <mamba::solver::Solution>`
|
||||
.. |UnSolvable| replace:: :cpp:type:`UnSolvable <mamba::solver::libsolv::UnSolvable>`
|
||||
|
||||
The :any:`libmambapy.solver <mamba::solver>` submodule contains a generic API for solving
|
||||
requirements (|MatchSpec|) into a list of packages (|PackageInfo|) with no conflicting dependencies.
|
||||
This problem is hard to solve (`NP-complete <https://en.wikipedia.org/wiki/NP-completeness>`_) which
|
||||
is why Mamba uses a `SAT solver <https://en.wikipedia.org/wiki/SAT_solver>`_ to do so.
|
||||
|
||||
.. note::
|
||||
|
||||
There is currently only one solver available in Mamba:
|
||||
`LibSolv <https://en.opensuse.org/openSUSE:Libzypp_satsolver>`_. For this reason, the generic
|
||||
interface has not been fully completed and users need to access the submodule
|
||||
:any:`libmambapy.solver.libsolv <mamba::solver::libsolv>` for certain types.
|
||||
|
||||
Populating the Package Database
|
||||
-------------------------------
|
||||
The first thing needed is a |Database| of all the packages and their dependencies.
|
||||
Packages are organised in repositories, described by a
|
||||
:cpp:type:`RepoInfo <mamba::solver::libsolv::RepoInfo>`.
|
||||
This serves to resolve explicit channel requirements or channel priority.
|
||||
As such, the database constructor takes a set of
|
||||
:cpp:type:`ChannelResolveParams <mamba::specs::ChannelResolveParams>`
|
||||
to work with :cpp:type:`Channel <mamba::specs::Channel>` work with Channel data
|
||||
internaly (see `the usage section on Channels <libmamba_usage_channel>`_ for more
|
||||
information).
|
||||
|
||||
The first way to add a repository is from a list of |PackageInfo| using
|
||||
:cpp:func:`DataBase.add_repo_from_packages <mamba::solver::libsolv::Database::add_repo_from_packages>`:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import libmambapy
|
||||
|
||||
db = libmambapy.solver.libsolv.Database(
|
||||
libmambapy.specs.ChannelResolveParams(channel_alias="https://conda.anaconda.org")
|
||||
)
|
||||
|
||||
repo1 = db.add_repo_from_packages(
|
||||
packages=[
|
||||
libmambapy.specs.PackageInfo(name="python", version="3.8", ...),
|
||||
libmambapy.specs.PackageInfo(name="pip", version="3.9", ...),
|
||||
...,
|
||||
],
|
||||
name="myrepo",
|
||||
)
|
||||
|
||||
The second way of loading packages is throuch Conda's reposoitory index format ``repodata.json``
|
||||
using
|
||||
:cpp:func:`DataBase.add_repo_from_repodata <mamba::solver::libsolv::Database::add_repo_from_repodata>`.
|
||||
This is meant as a convenience and performant alternative to the former method, since these files
|
||||
grow large.
|
||||
|
||||
.. code:: python
|
||||
|
||||
repo2 = db.add_repo_from_repodata(
|
||||
path="path/to/repodata.json",
|
||||
url="htts://conda.anaconda.org/conda-forge/linux-64",
|
||||
)
|
||||
|
||||
One of the reppository can be set to have a special meaning of "installed repository".
|
||||
It is used as a reference point in the solver to compute changes.
|
||||
For instance if a package is required but is already available in the installed repo, the solving
|
||||
result will not mention it.
|
||||
The function
|
||||
:cpp:func:`DataBase.set_installed_repo <mamba::solver::libsolv::Database::set_installed_repo>` is
|
||||
used for that purpose.
|
||||
|
||||
.. code:: python
|
||||
|
||||
db.set_installed_repo(repo1)
|
||||
|
||||
Binary serialization of the database (Advanced)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The |Database| reporitories can be serialized in binary format for faster reloading.
|
||||
To ensure integrity and freshness of the serialized file, metadata about the packages,
|
||||
such as source url and
|
||||
:cpp:type:`RepodataOrigin <mamba::solver::libsolv::RepodataOrigin>`, are stored inside the
|
||||
file when calling
|
||||
:cpp:func:`DataBase.native_serialize_repo <mamba::solver::libsolv::Database::native_serialize_repo>` .
|
||||
Upon reading, similar parameters are expected as input to
|
||||
:cpp:func:`DataBase.add_repo_from_native_serialization <mamba::solver::libsolv::Database::add_repo_from_native_serialization>`.
|
||||
If they mistmatch, the loading results in an error.
|
||||
|
||||
A typical wokflow first tries to load a repository from such binary cache, and then quietly
|
||||
fallbacks to ``repodata.json`` on failure.
|
||||
|
||||
Creating a solving request
|
||||
--------------------------
|
||||
All jobs that need to be resolved are added as part of a |Request|.
|
||||
This includes installing, updating, removing packages, as well as solving cutomization parameters.
|
||||
|
||||
.. code:: python
|
||||
|
||||
Request = libmambapy.solver.Request
|
||||
MatchSpec = libmambapy.specs.MatchSpec
|
||||
|
||||
request = Request(
|
||||
jobs=[
|
||||
Request.Install(MatchSpec.parse("python>=3.9")),
|
||||
Request.Update(MatchSpec.parse("numpy")),
|
||||
Request.Remove(MatchSpec.parse("pandas"), clean_dependencies=False),
|
||||
],
|
||||
flags=Request.Flags(
|
||||
allow_downgrade=True,
|
||||
allow_uninstall=True,
|
||||
),
|
||||
)
|
||||
|
||||
Solving the request
|
||||
-------------------
|
||||
The |Request| and the |Database| are the two input parameters needed to solve an environment.
|
||||
This task is achieve with the :cpp:func:`Solver.solve <mamba::solver::libsolv::Solver::solve>`
|
||||
method.
|
||||
|
||||
.. code:: python
|
||||
|
||||
solver = libmambapy.solver.libsolv.Solver()
|
||||
outcome = solver.solve(db, request)
|
||||
|
||||
The outcome can be of two types, either a |Solution| listing packages (|Packageinfo|) and the
|
||||
action to take on them (install, remove...), or an |UnSolvable| type when no solution exist
|
||||
(because of conflict, missing pacakges...).
|
||||
|
||||
Examine the solution
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
We can test if a valid solution exists by checking the type of the outcome.
|
||||
The attribute :cpp:member:`Solution.actions <mamba::solver::Solution::actions>` contains the actions
|
||||
to take on the installed repository so that it satisfies the |Request| requirements.
|
||||
|
||||
.. code:: python
|
||||
|
||||
Solution = libmambapy.solver.Solution
|
||||
|
||||
if isinstance(outcome, Solution):
|
||||
for action in outcome.actions:
|
||||
if isinstance(action, Solution.Upgrade):
|
||||
my_upgrade(from_pkg=action.remove, to_pkg=action.install)
|
||||
if isinstance(action, Solution.Reinstall):
|
||||
...
|
||||
...
|
||||
|
||||
Alternatively, an easy way to compute the update to the environment is to check for ``install`` and
|
||||
``remove`` members, since they will populate the relevant fields for all actions:
|
||||
|
||||
.. code:: python
|
||||
|
||||
Solution = libmambapy.solver.Solution
|
||||
|
||||
if isinstance(outcome, Solution):
|
||||
for action in outcome.actions:
|
||||
if hasattr(action, "install"):
|
||||
my_download_and_install(action.install)
|
||||
# WARN: Do not use `elif` since actions like `Upgrade`
|
||||
# are represented as an `install` and `remove` pair.
|
||||
if hasattr(action, "remove"):
|
||||
my_delete(action.remove)
|
||||
|
||||
Understand unsolvable problems
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
When a problem as no |Solution|, it is inherenty hard to come up with an explanation.
|
||||
In the easiest case, a requiered package is missing from the |Database|.
|
||||
In the most complex, many package dependencies are incompatible without a single culprit.
|
||||
In this case, packages should be rebuild with weaker requirements, or with more build variants.
|
||||
The |UnSolvable| class attempts to build an explanation.
|
||||
|
||||
The :cpp:func:`UnSolvable.problems <mamba::solver::libsolv::UnSolvable::problems>` is a list
|
||||
of problems, as defined by the solver.
|
||||
It is not easy to understand without linking it to specific |MatchSpec| and |PackageInfo|.
|
||||
The method
|
||||
:cpp:func:`UnSolvable.problems_graph <mamba::solver::libsolv::UnSolvable::problems_graph>`
|
||||
gives a more structured graph of package dependencies and incompatibilities.
|
||||
This graph is the underlying mechanism used in
|
||||
:cpp:func:`UnSolvable.explain_problems <mamba::solver::libsolv::UnSolvable::explain_problems>`
|
||||
to build a detail unsolvability message.
|
|
@ -1,7 +1,7 @@
|
|||
Describing Conda Objects
|
||||
========================
|
||||
|
||||
The ``libmambapy.specs`` submodule contains object to *describe* abstraction in the Conda ecosystem.
|
||||
The :any:`libmambapy.specs <mamba::specs>` submodule contains object to *describe* abstraction in the Conda ecosystem.
|
||||
They are purely functional and do not have any observable impact on the user system.
|
||||
For instance :cpp:type:`libmambapy.specs.Channel <mamba::specs::Channel>` is used to describe a
|
||||
channel but does not download any file.
|
||||
|
@ -124,6 +124,7 @@ Dynamic platforms (as in not known by Mamba) can only be detected with the ``[]`
|
|||
assert uc.type == specs.UnresolvedChannel.Type.Name
|
||||
|
||||
|
||||
.. _libmamba_usage_channel:
|
||||
Channel
|
||||
-------
|
||||
The :cpp:type:`Channel <mamba::specs::Channel>` are represented by a
|
||||
|
|
|
@ -79,17 +79,17 @@ namespace mamba::solver
|
|||
specs::MatchSpec spec;
|
||||
};
|
||||
|
||||
using Item = std::variant<Install, Remove, Update, UpdateAll, Keep, Freeze, Pin>;
|
||||
using item_list = std::vector<Item>;
|
||||
using Job = std::variant<Install, Remove, Update, UpdateAll, Keep, Freeze, Pin>;
|
||||
using job_list = std::vector<Job>;
|
||||
|
||||
Flags flags = {};
|
||||
item_list items = {};
|
||||
job_list jobs = {};
|
||||
};
|
||||
|
||||
template <typename... Item, typename Func>
|
||||
void for_each_of(const Request& request, Func&& func)
|
||||
{
|
||||
for (const auto& unknown_itm : request.items)
|
||||
for (const auto& unknown_job : request.jobs)
|
||||
{
|
||||
const auto control = std::visit(
|
||||
[&](const auto& itm) -> util::LoopControl
|
||||
|
@ -108,7 +108,7 @@ namespace mamba::solver
|
|||
}
|
||||
return util::LoopControl::Continue;
|
||||
},
|
||||
unknown_itm
|
||||
unknown_job
|
||||
);
|
||||
if (control == util::LoopControl::Break)
|
||||
{
|
||||
|
|
|
@ -406,7 +406,7 @@ namespace mamba
|
|||
const auto& prefix_pkgs = prefix_data.records();
|
||||
|
||||
auto request = Request();
|
||||
request.items.reserve(specs.size() + freeze_installed * prefix_pkgs.size());
|
||||
request.jobs.reserve(specs.size() + freeze_installed * prefix_pkgs.size());
|
||||
|
||||
// Consider if a FreezeAll type in Request is relevant?
|
||||
if (freeze_installed && !prefix_pkgs.empty())
|
||||
|
@ -414,13 +414,13 @@ namespace mamba
|
|||
LOG_INFO << "Locking environment: " << prefix_pkgs.size() << " packages freezed";
|
||||
for (const auto& [name, pkg] : prefix_pkgs)
|
||||
{
|
||||
request.items.emplace_back(Request::Freeze{ specs::MatchSpec::parse(name) });
|
||||
request.jobs.emplace_back(Request::Freeze{ specs::MatchSpec::parse(name) });
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& s : specs)
|
||||
{
|
||||
request.items.emplace_back(Request::Install{ specs::MatchSpec::parse(s) });
|
||||
request.jobs.emplace_back(Request::Install{ specs::MatchSpec::parse(s) });
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
@ -436,18 +436,16 @@ namespace mamba
|
|||
{
|
||||
using Request = solver::Request;
|
||||
|
||||
request.items.reserve(
|
||||
request.items.size() + (!no_pin) * ctx.pinned_packages.size() + !no_py_pin
|
||||
);
|
||||
request.jobs.reserve(request.jobs.size() + (!no_pin) * ctx.pinned_packages.size() + !no_py_pin);
|
||||
if (!no_pin)
|
||||
{
|
||||
for (const auto& pin : file_pins(prefix_data.path() / "conda-meta" / "pinned"))
|
||||
{
|
||||
request.items.emplace_back(Request::Pin{ specs::MatchSpec::parse(pin) });
|
||||
request.jobs.emplace_back(Request::Pin{ specs::MatchSpec::parse(pin) });
|
||||
}
|
||||
for (const auto& pin : ctx.pinned_packages)
|
||||
{
|
||||
request.items.emplace_back(Request::Pin{ specs::MatchSpec::parse(pin) });
|
||||
request.jobs.emplace_back(Request::Pin{ specs::MatchSpec::parse(pin) });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -456,14 +454,14 @@ namespace mamba
|
|||
auto py_pin = python_pin(prefix_data, specs);
|
||||
if (!py_pin.empty())
|
||||
{
|
||||
request.items.emplace_back(Request::Pin{ specs::MatchSpec::parse(py_pin) });
|
||||
request.jobs.emplace_back(Request::Pin{ specs::MatchSpec::parse(py_pin) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void print_request_pins_to(const solver::Request& request, std::ostream& out)
|
||||
{
|
||||
for (const auto& req : request.items)
|
||||
for (const auto& req : request.jobs)
|
||||
{
|
||||
bool first = true;
|
||||
std::visit(
|
||||
|
|
|
@ -76,24 +76,24 @@ namespace mamba
|
|||
using Request = solver::Request;
|
||||
|
||||
auto request = Request();
|
||||
request.items.reserve(raw_specs.size());
|
||||
request.jobs.reserve(raw_specs.size());
|
||||
|
||||
if (prune)
|
||||
{
|
||||
History history(ctx.prefix_params.target_prefix, channel_context);
|
||||
auto hist_map = history.get_requested_specs_map();
|
||||
|
||||
request.items.reserve(request.items.capacity() + hist_map.size());
|
||||
request.jobs.reserve(request.jobs.capacity() + hist_map.size());
|
||||
|
||||
for (auto& [name, spec] : hist_map)
|
||||
{
|
||||
request.items.emplace_back(Request::Keep{ std::move(spec) });
|
||||
request.jobs.emplace_back(Request::Keep{ std::move(spec) });
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& s : raw_specs)
|
||||
{
|
||||
request.items.emplace_back(Request::Remove{
|
||||
request.jobs.emplace_back(Request::Remove{
|
||||
specs::MatchSpec::parse(s),
|
||||
/* .clean_dependencies= */ prune,
|
||||
});
|
||||
|
|
|
@ -39,22 +39,22 @@ namespace mamba
|
|||
if (prune_deps)
|
||||
{
|
||||
auto hist_map = prefix_data.history().get_requested_specs_map();
|
||||
request.items.reserve(hist_map.size() + 1);
|
||||
request.jobs.reserve(hist_map.size() + 1);
|
||||
|
||||
for (auto& [name, spec] : hist_map)
|
||||
{
|
||||
request.items.emplace_back(Request::Keep{ std::move(spec) });
|
||||
request.jobs.emplace_back(Request::Keep{ std::move(spec) });
|
||||
}
|
||||
request.items.emplace_back(Request::UpdateAll{ /* .clean_dependencies= */ true });
|
||||
request.jobs.emplace_back(Request::UpdateAll{ /* .clean_dependencies= */ true });
|
||||
}
|
||||
else
|
||||
{
|
||||
request.items.emplace_back(Request::UpdateAll{ /* .clean_dependencies= */ false });
|
||||
request.jobs.emplace_back(Request::UpdateAll{ /* .clean_dependencies= */ false });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
request.items.reserve(specs.size());
|
||||
request.jobs.reserve(specs.size());
|
||||
if (remove_not_specified)
|
||||
{
|
||||
auto hist_map = prefix_data.history().get_requested_specs_map();
|
||||
|
@ -63,7 +63,7 @@ namespace mamba
|
|||
if (std::find(specs.begin(), specs.end(), it.second.name().str())
|
||||
== specs.end())
|
||||
{
|
||||
request.items.emplace_back(Request::Remove{
|
||||
request.jobs.emplace_back(Request::Remove{
|
||||
specs::MatchSpec::parse(it.second.name().str()),
|
||||
/* .clean_dependencies= */ true,
|
||||
});
|
||||
|
@ -73,7 +73,7 @@ namespace mamba
|
|||
|
||||
for (const auto& raw_ms : specs)
|
||||
{
|
||||
request.items.emplace_back(Request::Update{
|
||||
request.jobs.emplace_back(Request::Update{
|
||||
specs::MatchSpec::parse(raw_ms),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -963,11 +963,11 @@ namespace mamba::solver::libsolv
|
|||
};
|
||||
|
||||
auto iter = std::find_if(
|
||||
request.items.cbegin(),
|
||||
request.items.cend(),
|
||||
request.jobs.cbegin(),
|
||||
request.jobs.cend(),
|
||||
[&](const auto& unknown_job) { return std::visit(job_matches, unknown_job); }
|
||||
);
|
||||
return iter != request.items.cend();
|
||||
return iter != request.jobs.cend();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1177,83 +1177,84 @@ namespace mamba::solver::libsolv
|
|||
.transform([&](auto id) { jobs.push_back(install_flag, id); });
|
||||
}
|
||||
|
||||
template <typename Item>
|
||||
template <typename Job>
|
||||
[[nodiscard]] auto add_job(
|
||||
const Item& item,
|
||||
solv::ObjQueue& jobs,
|
||||
const Job& job,
|
||||
solv::ObjQueue& raw_jobs,
|
||||
solv::ObjPool& pool,
|
||||
const specs::ChannelResolveParams& params,
|
||||
bool force_reinstall
|
||||
) -> expected_t<void>
|
||||
{
|
||||
if constexpr (std::is_same_v<Item, Request::Install>)
|
||||
if constexpr (std::is_same_v<Job, Request::Install>)
|
||||
{
|
||||
if (force_reinstall)
|
||||
{
|
||||
return add_reinstall_job(jobs, pool, item.spec, params);
|
||||
return add_reinstall_job(raw_jobs, pool, job.spec, params);
|
||||
}
|
||||
else
|
||||
{
|
||||
return pool_add_matchspec(pool, item.spec, params)
|
||||
.transform([&](auto id)
|
||||
{ jobs.push_back(SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES, id); }
|
||||
return pool_add_matchspec(pool, job.spec, params)
|
||||
.transform(
|
||||
[&](auto id)
|
||||
{ raw_jobs.push_back(SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES, id); }
|
||||
);
|
||||
}
|
||||
}
|
||||
if constexpr (std::is_same_v<Item, Request::Remove>)
|
||||
if constexpr (std::is_same_v<Job, Request::Remove>)
|
||||
{
|
||||
return pool_add_matchspec(pool, item.spec, params)
|
||||
return pool_add_matchspec(pool, job.spec, params)
|
||||
.transform(
|
||||
[&](auto id)
|
||||
{
|
||||
jobs.push_back(
|
||||
raw_jobs.push_back(
|
||||
SOLVER_ERASE | SOLVER_SOLVABLE_PROVIDES
|
||||
| (item.clean_dependencies ? SOLVER_CLEANDEPS : 0),
|
||||
| (job.clean_dependencies ? SOLVER_CLEANDEPS : 0),
|
||||
id
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
if constexpr (std::is_same_v<Item, Request::Update>)
|
||||
if constexpr (std::is_same_v<Job, Request::Update>)
|
||||
{
|
||||
return pool_add_matchspec(pool, item.spec, params)
|
||||
return pool_add_matchspec(pool, job.spec, params)
|
||||
.transform(
|
||||
[&](auto id)
|
||||
{
|
||||
// TODO: ignoring update specs here for now
|
||||
if (!item.spec.is_simple())
|
||||
if (!job.spec.is_simple())
|
||||
{
|
||||
jobs.push_back(SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES, id);
|
||||
raw_jobs.push_back(SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES, id);
|
||||
}
|
||||
jobs.push_back(SOLVER_UPDATE | SOLVER_SOLVABLE_PROVIDES, id);
|
||||
raw_jobs.push_back(SOLVER_UPDATE | SOLVER_SOLVABLE_PROVIDES, id);
|
||||
}
|
||||
);
|
||||
}
|
||||
if constexpr (std::is_same_v<Item, Request::UpdateAll>)
|
||||
if constexpr (std::is_same_v<Job, Request::UpdateAll>)
|
||||
{
|
||||
jobs.push_back(
|
||||
raw_jobs.push_back(
|
||||
SOLVER_UPDATE | SOLVER_SOLVABLE_ALL
|
||||
| (item.clean_dependencies ? SOLVER_CLEANDEPS : 0),
|
||||
| (job.clean_dependencies ? SOLVER_CLEANDEPS : 0),
|
||||
0
|
||||
);
|
||||
return {};
|
||||
}
|
||||
if constexpr (std::is_same_v<Item, Request::Freeze>)
|
||||
if constexpr (std::is_same_v<Job, Request::Freeze>)
|
||||
{
|
||||
return pool_add_matchspec(pool, item.spec, params)
|
||||
.transform([&](auto id) { jobs.push_back(SOLVER_LOCK, id); });
|
||||
return pool_add_matchspec(pool, job.spec, params)
|
||||
.transform([&](auto id) { raw_jobs.push_back(SOLVER_LOCK, id); });
|
||||
}
|
||||
if constexpr (std::is_same_v<Item, Request::Keep>)
|
||||
if constexpr (std::is_same_v<Job, Request::Keep>)
|
||||
{
|
||||
jobs.push_back(
|
||||
raw_jobs.push_back(
|
||||
SOLVER_USERINSTALLED,
|
||||
pool_add_matchspec(pool, item.spec, params).value()
|
||||
pool_add_matchspec(pool, job.spec, params).value()
|
||||
);
|
||||
return {};
|
||||
}
|
||||
if constexpr (std::is_same_v<Item, Request::Pin>)
|
||||
if constexpr (std::is_same_v<Job, Request::Pin>)
|
||||
{
|
||||
return pool_add_pin(pool, item.spec, params)
|
||||
return pool_add_pin(pool, job.spec, params)
|
||||
.transform(
|
||||
[&](solv::ObjSolvableView pin_solv)
|
||||
{
|
||||
|
@ -1261,9 +1262,9 @@ namespace mamba::solver::libsolv
|
|||
// WARNING keep separate or libsolv does not understand
|
||||
// Force verify the dummy solvable dependencies, as this is not the
|
||||
// default for installed packages.
|
||||
jobs.push_back(SOLVER_VERIFY, name_id);
|
||||
raw_jobs.push_back(SOLVER_VERIFY, name_id);
|
||||
// Lock the dummy solvable so that it stays install.
|
||||
jobs.push_back(SOLVER_LOCK, name_id);
|
||||
raw_jobs.push_back(SOLVER_LOCK, name_id);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -1282,18 +1283,18 @@ namespace mamba::solver::libsolv
|
|||
auto solv_jobs = solv::ObjQueue();
|
||||
|
||||
auto error = expected_t<void>();
|
||||
for (const auto& item : request.items)
|
||||
for (const auto& unknown_job : request.jobs)
|
||||
{
|
||||
auto xpt = std::visit(
|
||||
[&](const auto& r) -> expected_t<void>
|
||||
[&](const auto& job) -> expected_t<void>
|
||||
{
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(r)>, Request::Pin>)
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(job)>, Request::Pin>)
|
||||
{
|
||||
return add_job(r, solv_jobs, pool, chan_params, force_reinstall);
|
||||
return add_job(job, solv_jobs, pool, chan_params, force_reinstall);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
item
|
||||
unknown_job
|
||||
);
|
||||
if (!xpt)
|
||||
{
|
||||
|
@ -1304,18 +1305,18 @@ namespace mamba::solver::libsolv
|
|||
// Channel specific MatchSpec write to whatprovides and hence require it is not modified
|
||||
// afterwards.
|
||||
pool.create_whatprovides();
|
||||
for (const auto& item : request.items)
|
||||
for (const auto& unkown_job : request.jobs)
|
||||
{
|
||||
auto xpt = std::visit(
|
||||
[&](const auto& r) -> expected_t<void>
|
||||
[&](const auto& job) -> expected_t<void>
|
||||
{
|
||||
if constexpr (!std::is_same_v<std::decay_t<decltype(r)>, Request::Pin>)
|
||||
if constexpr (!std::is_same_v<std::decay_t<decltype(job)>, Request::Pin>)
|
||||
{
|
||||
return add_job(r, solv_jobs, pool, chan_params, force_reinstall);
|
||||
return add_job(job, solv_jobs, pool, chan_params, force_reinstall);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
item
|
||||
unkown_job
|
||||
);
|
||||
if (!xpt)
|
||||
{
|
||||
|
|
|
@ -90,7 +90,7 @@ namespace mamba::solver::libsolv
|
|||
{
|
||||
if (request.flags.order_request)
|
||||
{
|
||||
std::sort(request.items.begin(), request.items.end(), make_request_cmp());
|
||||
std::sort(request.jobs.begin(), request.jobs.end(), make_request_cmp());
|
||||
}
|
||||
return solve_impl(mpool, request);
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ namespace mamba::solver::libsolv
|
|||
if (request.flags.order_request)
|
||||
{
|
||||
auto sorted_request = request;
|
||||
std::sort(sorted_request.items.begin(), sorted_request.items.end(), make_request_cmp());
|
||||
std::sort(sorted_request.jobs.begin(), sorted_request.jobs.end(), make_request_cmp());
|
||||
return solve_impl(mpool, sorted_request);
|
||||
}
|
||||
return solve_impl(mpool, request);
|
||||
|
|
|
@ -53,6 +53,7 @@ set(
|
|||
src/solver/test_problems_graph.cpp
|
||||
# Solver libsolv implementation tests
|
||||
src/solver/libsolv/test_database.cpp
|
||||
src/solver/libsolv/test_solver.cpp
|
||||
# Artifacts validation
|
||||
src/validation/test_tools.cpp
|
||||
src/validation/test_update_framework_v0_6.cpp
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
"build": "conda_forge",
|
||||
"build_number": 0,
|
||||
"build_string": "conda_forge",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": null,
|
||||
"fn": "_libgcc_mutex-0.1-conda_forge.tar.bz2",
|
||||
|
@ -23,7 +22,6 @@
|
|||
"build": "2_gnu",
|
||||
"build_number": 16,
|
||||
"build_string": "2_gnu",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"openmp_impl 9999"
|
||||
],
|
||||
|
@ -47,7 +45,6 @@
|
|||
"build": "h7f98852_5",
|
||||
"build_number": 5,
|
||||
"build_string": "h7f98852_5",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=9.4.0"
|
||||
|
@ -68,7 +65,6 @@
|
|||
"build": "h166bdaf_0",
|
||||
"build_number": 0,
|
||||
"build_string": "h166bdaf_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=12"
|
||||
|
@ -91,7 +87,6 @@
|
|||
"build": "hd590300_5",
|
||||
"build_number": 5,
|
||||
"build_string": "hd590300_5",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=12"
|
||||
|
@ -112,7 +107,6 @@
|
|||
"build": "hbcca054_0",
|
||||
"build_number": 0,
|
||||
"build_string": "hbcca054_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": null,
|
||||
"fn": "ca-certificates-2024.2.2-hbcca054_0.conda",
|
||||
|
@ -131,7 +125,6 @@
|
|||
"build": "h41732ed_0",
|
||||
"build_number": 0,
|
||||
"build_string": "h41732ed_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"binutils_impl_linux-64 2.40"
|
||||
],
|
||||
|
@ -152,7 +145,6 @@
|
|||
"build": "21_linux64_openblas",
|
||||
"build_number": 21,
|
||||
"build_string": "21_linux64_openblas",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"liblapacke 3.9.0 21_linux64_openblas",
|
||||
"blas * openblas",
|
||||
|
@ -179,7 +171,6 @@
|
|||
"build": "21_linux64_openblas",
|
||||
"build_number": 21,
|
||||
"build_string": "21_linux64_openblas",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"liblapacke 3.9.0 21_linux64_openblas",
|
||||
"blas * openblas",
|
||||
|
@ -204,7 +195,6 @@
|
|||
"build": "hcb278e6_1",
|
||||
"build_number": 1,
|
||||
"build_string": "hcb278e6_1",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"expat 2.5.0.*"
|
||||
],
|
||||
|
@ -227,7 +217,6 @@
|
|||
"build": "h807b86a_5",
|
||||
"build_number": 5,
|
||||
"build_string": "h807b86a_5",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"libgomp 13.2.0 h807b86a_5"
|
||||
],
|
||||
|
@ -251,7 +240,6 @@
|
|||
"build": "h69a702a_5",
|
||||
"build_number": 5,
|
||||
"build_string": "h69a702a_5",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgfortran5 13.2.0 ha4646dd_5"
|
||||
|
@ -272,7 +260,6 @@
|
|||
"build": "ha4646dd_5",
|
||||
"build_number": 5,
|
||||
"build_string": "ha4646dd_5",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"libgfortran-ng 13.2.0"
|
||||
],
|
||||
|
@ -295,7 +282,6 @@
|
|||
"build": "h807b86a_5",
|
||||
"build_number": 5,
|
||||
"build_string": "h807b86a_5",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"_libgcc_mutex 0.1 conda_forge"
|
||||
|
@ -316,7 +302,6 @@
|
|||
"build": "21_linux64_openblas",
|
||||
"build_number": 21,
|
||||
"build_string": "21_linux64_openblas",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"liblapacke 3.9.0 21_linux64_openblas",
|
||||
"libcblas 3.9.0 21_linux64_openblas",
|
||||
|
@ -341,7 +326,6 @@
|
|||
"build": "hd590300_0",
|
||||
"build_number": 0,
|
||||
"build_string": "hd590300_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=12"
|
||||
|
@ -362,7 +346,6 @@
|
|||
"build": "pthreads_h413a1c8_0",
|
||||
"build_number": 0,
|
||||
"build_string": "pthreads_h413a1c8_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"openblas >=0.3.26,<0.3.27.0a0"
|
||||
],
|
||||
|
@ -387,7 +370,6 @@
|
|||
"build": "h2797004_0",
|
||||
"build_number": 0,
|
||||
"build_string": "h2797004_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=12",
|
||||
|
@ -409,7 +391,6 @@
|
|||
"build": "h7e041cc_5",
|
||||
"build_number": 5,
|
||||
"build_string": "h7e041cc_5",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": null,
|
||||
"fn": "libstdcxx-ng-13.2.0-h7e041cc_5.conda",
|
||||
|
@ -428,7 +409,6 @@
|
|||
"build": "h0b41bf4_0",
|
||||
"build_number": 0,
|
||||
"build_string": "h0b41bf4_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=12"
|
||||
|
@ -449,7 +429,6 @@
|
|||
"build": "hd590300_1",
|
||||
"build_number": 1,
|
||||
"build_string": "hd590300_1",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=12"
|
||||
|
@ -470,7 +449,6 @@
|
|||
"build": "hd590300_5",
|
||||
"build_number": 5,
|
||||
"build_string": "hd590300_5",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"zlib 1.2.13 *_5"
|
||||
],
|
||||
|
@ -493,7 +471,6 @@
|
|||
"build": "h59595ed_2",
|
||||
"build_number": 2,
|
||||
"build_string": "h59595ed_2",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=12"
|
||||
|
@ -514,7 +491,6 @@
|
|||
"build": "py312heda63a1_0",
|
||||
"build_number": 0,
|
||||
"build_string": "py312heda63a1_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"numpy-base <0a0"
|
||||
],
|
||||
|
@ -543,7 +519,6 @@
|
|||
"build": "hd590300_0",
|
||||
"build_number": 0,
|
||||
"build_string": "hd590300_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"pyopenssl >=22.1"
|
||||
],
|
||||
|
@ -567,7 +542,6 @@
|
|||
"build": "pyhd8ed1ab_0",
|
||||
"build_number": 0,
|
||||
"build_string": "pyhd8ed1ab_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/noarch",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"setuptools",
|
||||
|
@ -591,7 +565,6 @@
|
|||
"build": "hab00c5b_1_cpython",
|
||||
"build_number": 1,
|
||||
"build_string": "hab00c5b_1_cpython",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"python_abi 3.12.* *_cp312"
|
||||
],
|
||||
|
@ -629,7 +602,6 @@
|
|||
"build": "4_cp312",
|
||||
"build_number": 4,
|
||||
"build_string": "4_cp312",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": [
|
||||
"python 3.12.* *_cpython"
|
||||
],
|
||||
|
@ -650,7 +622,6 @@
|
|||
"build": "h8228510_1",
|
||||
"build_number": 1,
|
||||
"build_string": "h8228510_1",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=12",
|
||||
|
@ -672,7 +643,6 @@
|
|||
"build": "pyhd8ed1ab_0",
|
||||
"build_number": 0,
|
||||
"build_string": "pyhd8ed1ab_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/noarch",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"python >=3.7"
|
||||
|
@ -694,7 +664,6 @@
|
|||
"build": "noxft_h4845f30_101",
|
||||
"build_number": 101,
|
||||
"build_string": "noxft_h4845f30_101",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/linux-64",
|
||||
"constrains": null,
|
||||
"depends": [
|
||||
"libgcc-ng >=12",
|
||||
|
@ -716,7 +685,6 @@
|
|||
"build": "h0c530f3_0",
|
||||
"build_number": 0,
|
||||
"build_string": "h0c530f3_0",
|
||||
"channel": "https://conda.anaconda.org/conda-forge/noarch",
|
||||
"constrains": null,
|
||||
"depends": null,
|
||||
"fn": "tzdata-2024a-h0c530f3_0.conda",
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
#include "mamba/specs/package_info.hpp"
|
||||
#include "mamba/util/string.hpp"
|
||||
|
||||
#include "mambatests.hpp"
|
||||
|
||||
using namespace mamba;
|
||||
using namespace mamba::solver;
|
||||
|
||||
|
@ -172,7 +174,7 @@ TEST_SUITE("solver::libsolv::database")
|
|||
|
||||
SUBCASE("Add repo from repodata with no extra pip")
|
||||
{
|
||||
const auto repodata = fs::u8path(MAMBA_TEST_DATA_DIR)
|
||||
const auto repodata = mambatests::test_data_dir
|
||||
/ "repodata/conda-forge-numpy-linux-64.json";
|
||||
auto repo1 = db.add_repo_from_repodata_json(
|
||||
repodata,
|
||||
|
@ -200,7 +202,7 @@ TEST_SUITE("solver::libsolv::database")
|
|||
|
||||
SUBCASE("Add repo from repodata with extra pip")
|
||||
{
|
||||
const auto repodata = fs::u8path(MAMBA_TEST_DATA_DIR)
|
||||
const auto repodata = mambatests::test_data_dir
|
||||
/ "repodata/conda-forge-numpy-linux-64.json";
|
||||
auto repo1 = db.add_repo_from_repodata_json(
|
||||
repodata,
|
||||
|
@ -230,7 +232,7 @@ TEST_SUITE("solver::libsolv::database")
|
|||
|
||||
SUBCASE("Add repo from repodata only .tar.bz2")
|
||||
{
|
||||
const auto repodata = fs::u8path(MAMBA_TEST_DATA_DIR)
|
||||
const auto repodata = mambatests::test_data_dir
|
||||
/ "repodata/conda-forge-numpy-linux-64.json";
|
||||
auto repo1 = db.add_repo_from_repodata_json(
|
||||
repodata,
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
// 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 <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "mamba/fs/filesystem.hpp"
|
||||
#include "mamba/solver/libsolv/database.hpp"
|
||||
#include "mamba/solver/libsolv/solver.hpp"
|
||||
#include "mamba/specs/channel.hpp"
|
||||
|
||||
#include "mambatests.hpp"
|
||||
|
||||
using namespace mamba;
|
||||
using namespace mamba::solver;
|
||||
|
||||
auto
|
||||
find_actions_with_name(const Solution& solution, std::string_view name)
|
||||
-> std::vector<Solution::Action>
|
||||
{
|
||||
auto out = std::vector<Solution::Action>();
|
||||
for (const auto& action : solution.actions)
|
||||
{
|
||||
std::visit(
|
||||
[&](const auto& act)
|
||||
{
|
||||
using Act = std::decay_t<decltype(act)>;
|
||||
if constexpr (Solution::has_remove_v<Act>)
|
||||
{
|
||||
if (act.remove.name == name)
|
||||
{
|
||||
out.push_back(act);
|
||||
}
|
||||
}
|
||||
else if constexpr (Solution::has_install_v<Act>)
|
||||
{
|
||||
if (act.install.name == name)
|
||||
{
|
||||
out.push_back(act);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (act.what.name == name)
|
||||
{
|
||||
out.push_back(act);
|
||||
}
|
||||
}
|
||||
},
|
||||
action
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
TEST_SUITE("solver::libsolv::solver")
|
||||
{
|
||||
using namespace specs::match_spec_literals;
|
||||
|
||||
TEST_CASE("Solve a fresh environment with one repository")
|
||||
{
|
||||
auto db = libsolv::Database({});
|
||||
|
||||
// A conda-forge/linux-64 subsample with one version of numpy and pip and their dependencies
|
||||
auto repo = db.add_repo_from_repodata_json(
|
||||
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
|
||||
"https://conda.anaconda.org/conda-forge/linux-64",
|
||||
libsolv::PipAsPythonDependency::No
|
||||
);
|
||||
REQUIRE(repo.has_value());
|
||||
|
||||
SUBCASE("Install numpy")
|
||||
{
|
||||
const auto request = Request{
|
||||
/* .flags= */ {},
|
||||
/* .jobs= */ { Request::Install{ "numpy"_ms } },
|
||||
};
|
||||
const auto outcome = libsolv::Solver().solve(db, request);
|
||||
|
||||
REQUIRE(outcome.has_value());
|
||||
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
|
||||
const auto& solution = std::get<Solution>(outcome.value());
|
||||
|
||||
REQUIRE_FALSE(solution.actions.empty());
|
||||
// Numpy is last because of topological sort
|
||||
CHECK(std::holds_alternative<Solution::Install>(solution.actions.back()));
|
||||
CHECK_EQ(std::get<Solution::Install>(solution.actions.back()).install.name, "numpy");
|
||||
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
|
||||
|
||||
const auto python_actions = find_actions_with_name(solution, "python");
|
||||
REQUIRE_EQ(python_actions.size(), 1);
|
||||
CHECK(std::holds_alternative<Solution::Install>(python_actions.front()));
|
||||
}
|
||||
|
||||
SUBCASE("Install numpy without dependencies")
|
||||
{
|
||||
const auto request = Request{
|
||||
/* .flags= */ {
|
||||
/* .keep_dependencies */ false,
|
||||
/* .keep_user_specs */ true,
|
||||
},
|
||||
/* .jobs= */ { Request::Install{ "numpy"_ms } },
|
||||
};
|
||||
const auto outcome = libsolv::Solver().solve(db, request);
|
||||
|
||||
REQUIRE(outcome.has_value());
|
||||
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
|
||||
const auto& solution = std::get<Solution>(outcome.value());
|
||||
|
||||
REQUIRE_FALSE(solution.actions.empty());
|
||||
// Numpy is last because of topological sort
|
||||
CHECK(std::holds_alternative<Solution::Install>(solution.actions.back()));
|
||||
CHECK_EQ(std::get<Solution::Install>(solution.actions.back()).install.name, "numpy");
|
||||
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
|
||||
|
||||
const auto python_actions = find_actions_with_name(solution, "python");
|
||||
REQUIRE_EQ(python_actions.size(), 1);
|
||||
CHECK(std::holds_alternative<Solution::Omit>(python_actions.front()));
|
||||
}
|
||||
|
||||
SUBCASE("Install numpy dependencies only")
|
||||
{
|
||||
const auto request = Request{
|
||||
/* .flags= */ {
|
||||
/* .keep_dependencies */ true,
|
||||
/* .keep_user_specs */ false,
|
||||
},
|
||||
/* .jobs= */ { Request::Install{ "numpy"_ms } },
|
||||
};
|
||||
const auto outcome = libsolv::Solver().solve(db, request);
|
||||
|
||||
REQUIRE(outcome.has_value());
|
||||
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
|
||||
const auto& solution = std::get<Solution>(outcome.value());
|
||||
|
||||
REQUIRE_FALSE(solution.actions.empty());
|
||||
// Numpy is last because of topological sort
|
||||
CHECK(std::holds_alternative<Solution::Omit>(solution.actions.back()));
|
||||
CHECK_EQ(std::get<Solution::Omit>(solution.actions.back()).what.name, "numpy");
|
||||
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
|
||||
|
||||
const auto python_actions = find_actions_with_name(solution, "python");
|
||||
REQUIRE_EQ(python_actions.size(), 1);
|
||||
CHECK(std::holds_alternative<Solution::Install>(python_actions.front()));
|
||||
|
||||
// Pip is not a dependency of numpy (or python here)
|
||||
CHECK(find_actions_with_name(solution, "pip").empty());
|
||||
}
|
||||
|
||||
SUBCASE("Fail to install missing package")
|
||||
{
|
||||
const auto request = Request{
|
||||
/* .flags= */ {},
|
||||
/* .jobs= */ { Request::Install{ "does-not-exist"_ms } },
|
||||
};
|
||||
const auto outcome = libsolv::Solver().solve(db, request);
|
||||
|
||||
REQUIRE(outcome.has_value());
|
||||
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Remove packages")
|
||||
{
|
||||
auto db = libsolv::Database({});
|
||||
|
||||
// A conda-forge/linux-64 subsample with one version of numpy and pip and their dependencies
|
||||
const auto repo = db.add_repo_from_repodata_json(
|
||||
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
|
||||
"https://conda.anaconda.org/conda-forge/linux-64"
|
||||
);
|
||||
REQUIRE(repo.has_value());
|
||||
db.set_installed_repo(repo.value());
|
||||
|
||||
SUBCASE("Remove numpy and dependencies")
|
||||
{
|
||||
const auto request = Request{
|
||||
/* .flags= */ {},
|
||||
/* .jobs= */ { Request::Remove{ "numpy"_ms, true } },
|
||||
};
|
||||
const auto outcome = libsolv::Solver().solve(db, request);
|
||||
|
||||
REQUIRE(outcome.has_value());
|
||||
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
|
||||
const auto& solution = std::get<Solution>(outcome.value());
|
||||
|
||||
REQUIRE_FALSE(solution.actions.empty());
|
||||
// Numpy is first because of topological sort
|
||||
CHECK(std::holds_alternative<Solution::Remove>(solution.actions.front()));
|
||||
CHECK_EQ(std::get<Solution::Remove>(solution.actions.front()).remove.name, "numpy");
|
||||
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
|
||||
|
||||
// Python is not removed because it is needed by pip which is installed
|
||||
CHECK(find_actions_with_name(solution, "pip").empty());
|
||||
}
|
||||
|
||||
SUBCASE("Remove numpy and pip and dependencies")
|
||||
{
|
||||
const auto request = Request{
|
||||
/* .flags= */ {},
|
||||
/* .jobs= */ { Request::Remove{ "numpy"_ms, true }, Request::Remove{ "pip"_ms, true } },
|
||||
};
|
||||
const auto outcome = libsolv::Solver().solve(db, request);
|
||||
|
||||
REQUIRE(outcome.has_value());
|
||||
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
|
||||
const auto& solution = std::get<Solution>(outcome.value());
|
||||
|
||||
const auto numpy_actions = find_actions_with_name(solution, "numpy");
|
||||
REQUIRE_EQ(numpy_actions.size(), 1);
|
||||
CHECK(std::holds_alternative<Solution::Remove>(numpy_actions.front()));
|
||||
|
||||
const auto pip_actions = find_actions_with_name(solution, "pip");
|
||||
REQUIRE_EQ(pip_actions.size(), 1);
|
||||
CHECK(std::holds_alternative<Solution::Remove>(pip_actions.front()));
|
||||
|
||||
const auto python_actions = find_actions_with_name(solution, "python");
|
||||
REQUIRE_EQ(python_actions.size(), 1);
|
||||
CHECK(std::holds_alternative<Solution::Remove>(python_actions.front()));
|
||||
}
|
||||
|
||||
SUBCASE("Remove numpy without dependencies")
|
||||
{
|
||||
const auto request = Request{
|
||||
/* .flags= */ {},
|
||||
/* .jobs= */ { Request::Remove{ "numpy"_ms, false } },
|
||||
};
|
||||
const auto outcome = libsolv::Solver().solve(db, request);
|
||||
|
||||
REQUIRE(outcome.has_value());
|
||||
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
|
||||
const auto& solution = std::get<Solution>(outcome.value());
|
||||
|
||||
REQUIRE_EQ(solution.actions.size(), 1);
|
||||
CHECK(std::holds_alternative<Solution::Remove>(solution.actions.front()));
|
||||
CHECK_EQ(std::get<Solution::Remove>(solution.actions.front()).remove.name, "numpy");
|
||||
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
|
||||
}
|
||||
|
||||
SUBCASE("Removing non-existing package is a no-op")
|
||||
{
|
||||
const auto request = Request{
|
||||
/* .flags= */ {},
|
||||
/* .jobs= */ { Request::Remove{ "does-not-exist"_ms } },
|
||||
};
|
||||
const auto outcome = libsolv::Solver().solve(db, request);
|
||||
|
||||
REQUIRE(outcome.has_value());
|
||||
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
|
||||
const auto& solution = std::get<Solution>(outcome.value());
|
||||
|
||||
CHECK(solution.actions.empty());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,10 +17,10 @@
|
|||
|
||||
namespace mamba::solver
|
||||
{
|
||||
// Fix Pybind11 py::bind_vector<Request::item_list> has trouble detecting the abscence
|
||||
// Fix Pybind11 py::bind_vector<Request::job_list> has trouble detecting the abscence
|
||||
// of comparions operators, so we tell it explicitly.
|
||||
auto operator==(const Request::Item&, const Request::Item&) -> bool = delete;
|
||||
auto operator!=(const Request::Item&, const Request::Item&) -> bool = delete;
|
||||
auto operator==(const Request::Job&, const Request::Job&) -> bool = delete;
|
||||
auto operator!=(const Request::Job&, const Request::Job&) -> bool = delete;
|
||||
|
||||
// Fix Pybind11 py::bind_vector<Solution::actions> has trouble detecting the abscence
|
||||
// of comparions operators, so we tell it explicitly.
|
||||
|
@ -28,7 +28,7 @@ namespace mamba::solver
|
|||
auto operator!=(const Solution::Action&, const Solution::Action&) -> bool = delete;
|
||||
}
|
||||
|
||||
PYBIND11_MAKE_OPAQUE(mamba::solver::Request::item_list);
|
||||
PYBIND11_MAKE_OPAQUE(mamba::solver::Request::job_list);
|
||||
PYBIND11_MAKE_OPAQUE(mamba::solver::Solution::action_list);
|
||||
|
||||
namespace mambapy
|
||||
|
@ -113,7 +113,7 @@ namespace mambapy
|
|||
.def("__deepcopy__", &deepcopy<Request::Pin>, py::arg("memo"));
|
||||
|
||||
// Type made opaque at the top of this file
|
||||
py::bind_vector<Request::item_list>(py_request, "ItemList");
|
||||
py::bind_vector<Request::job_list>(py_request, "JobList");
|
||||
|
||||
py::class_<Request::Flags>(py_request, "Flags")
|
||||
.def(
|
||||
|
@ -159,25 +159,31 @@ namespace mambapy
|
|||
.def(
|
||||
// Big copy unfortunately
|
||||
py::init(
|
||||
[](Request::Flags flags, Request::item_list items) -> Request {
|
||||
return { std::move(flags), std::move(items) };
|
||||
[](Request::job_list jobs, Request::Flags flags) -> Request {
|
||||
return { std::move(flags), std::move(jobs) };
|
||||
}
|
||||
)
|
||||
),
|
||||
py::arg("jobs"),
|
||||
py::arg("flags") = Request::Flags()
|
||||
)
|
||||
.def(py::init(
|
||||
[](py::iterable items) -> Request
|
||||
{
|
||||
auto request = Request();
|
||||
request.items.reserve(py::len_hint(items));
|
||||
for (py::handle itm : items)
|
||||
.def(
|
||||
py::init(
|
||||
[](py::iterable jobs, Request::Flags flags) -> Request
|
||||
{
|
||||
request.items.push_back(py::cast<Request::Item>(itm));
|
||||
auto request = Request{ std::move(flags) };
|
||||
request.jobs.reserve(py::len_hint(jobs));
|
||||
for (py::handle itm : jobs)
|
||||
{
|
||||
request.jobs.push_back(py::cast<Request::Job>(itm));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
))
|
||||
),
|
||||
py::arg("jobs"),
|
||||
py::arg("flags") = Request::Flags()
|
||||
)
|
||||
.def_readwrite("flags", &Request::flags)
|
||||
.def_readwrite("items", &Request::items)
|
||||
.def_readwrite("jobs", &Request::jobs)
|
||||
.def("__copy__", ©<Request>)
|
||||
.def("__deepcopy__", &deepcopy<Request>, py::arg("memo"));
|
||||
|
||||
|
@ -335,12 +341,18 @@ namespace mambapy
|
|||
|
||||
py::class_<ProblemsGraph::RootNode>(py_problems_graph, "RootNode") //
|
||||
.def(py::init<>());
|
||||
py::class_<ProblemsGraph::PackageNode, specs::PackageInfo>(py_problems_graph, "PackageNode");
|
||||
|
||||
py::class_<ProblemsGraph::PackageNode, specs::PackageInfo>(py_problems_graph, "PackageNode")
|
||||
.def(py::init<specs::PackageInfo>());
|
||||
|
||||
py::class_<ProblemsGraph::UnresolvedDependencyNode, specs::MatchSpec>(
|
||||
py_problems_graph,
|
||||
"UnresolvedDependencyNode"
|
||||
);
|
||||
py::class_<ProblemsGraph::ConstraintNode, specs::MatchSpec>(py_problems_graph, "ConstraintNode");
|
||||
)
|
||||
.def(py::init<specs::MatchSpec>());
|
||||
|
||||
py::class_<ProblemsGraph::ConstraintNode, specs::MatchSpec>(py_problems_graph, "ConstraintNode")
|
||||
.def(py::init<specs::MatchSpec>());
|
||||
|
||||
py::class_<ProblemsGraph::conflicts_t>(py_problems_graph, "ConflictMap")
|
||||
.def(py::init([]() { return ProblemsGraph::conflicts_t(); }))
|
||||
|
@ -373,9 +385,8 @@ namespace mambapy
|
|||
auto const& g = self.graph();
|
||||
return std::pair(g.nodes(), g.edges());
|
||||
}
|
||||
);
|
||||
|
||||
m.def("simplify_conflicts", &solver::simplify_conflicts);
|
||||
)
|
||||
.def_static("simplify_conflicts", &solver::simplify_conflicts);
|
||||
|
||||
auto py_compressed_problems_graph = py::class_<CompressedProblemsGraph>(
|
||||
m,
|
||||
|
@ -448,11 +459,17 @@ namespace mambapy
|
|||
);
|
||||
|
||||
py_compressed_problems_graph
|
||||
.def_static("from_problems_graph", &CompressedProblemsGraph::from_problems_graph)
|
||||
.def_static(
|
||||
"from_problems_graph",
|
||||
&CompressedProblemsGraph::from_problems_graph,
|
||||
py::arg("problems_graph"),
|
||||
py::arg("merge_criteria")
|
||||
)
|
||||
.def_static(
|
||||
"from_problems_graph",
|
||||
[](const ProblemsGraph& pbs)
|
||||
{ return CompressedProblemsGraph::from_problems_graph(pbs); }
|
||||
{ return CompressedProblemsGraph::from_problems_graph(pbs); },
|
||||
py::arg("problems_graph")
|
||||
)
|
||||
.def("root_node", &CompressedProblemsGraph::root_node)
|
||||
.def("conflicts", &CompressedProblemsGraph::conflicts)
|
||||
|
|
|
@ -21,7 +21,7 @@ def test_import_recursive():
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"Item",
|
||||
"Job",
|
||||
[
|
||||
libmambapy.solver.Request.Install,
|
||||
libmambapy.solver.Request.Remove,
|
||||
|
@ -31,8 +31,8 @@ def test_import_recursive():
|
|||
libmambapy.solver.Request.Pin,
|
||||
],
|
||||
)
|
||||
def test_Request_Item_spec(Item):
|
||||
itm = Item(spec=libmambapy.specs.MatchSpec.parse("foo"))
|
||||
def test_Request_Job_spec(Job):
|
||||
itm = Job(spec=libmambapy.specs.MatchSpec.parse("foo"))
|
||||
|
||||
assert str(itm.spec) == "foo"
|
||||
|
||||
|
@ -47,14 +47,14 @@ def test_Request_Item_spec(Item):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["Item", "kwargs"],
|
||||
["Job", "kwargs"],
|
||||
[
|
||||
(libmambapy.solver.Request.Remove, {"spec": libmambapy.specs.MatchSpec.parse("foo")}),
|
||||
(libmambapy.solver.Request.UpdateAll, {}),
|
||||
],
|
||||
)
|
||||
def test_Request_Item_clean(Item, kwargs):
|
||||
itm = Item(**kwargs, clean_dependencies=False)
|
||||
def test_Request_Job_clean(Job, kwargs):
|
||||
itm = Job(**kwargs, clean_dependencies=False)
|
||||
|
||||
assert not itm.clean_dependencies
|
||||
|
||||
|
@ -94,6 +94,31 @@ def test_Request_Flags_boolean(attr):
|
|||
assert getattr(flags, attr) == val
|
||||
|
||||
|
||||
def test_Request():
|
||||
Request = libmambapy.solver.Request
|
||||
MatchSpec = libmambapy.specs.MatchSpec
|
||||
|
||||
request = Request(
|
||||
jobs=[Request.Install(MatchSpec.parse("foo"))],
|
||||
flags=Request.Flags(keep_dependencies=False),
|
||||
)
|
||||
|
||||
# Getters
|
||||
assert len(request.jobs) == 1
|
||||
assert not request.flags.keep_dependencies
|
||||
|
||||
# Setters
|
||||
request.jobs.append(Request.Remove(MatchSpec.parse("bar<2.0")))
|
||||
assert len(request.jobs) == 2
|
||||
request.flags.keep_dependencies = True
|
||||
assert request.flags.keep_dependencies
|
||||
|
||||
# Copy
|
||||
other = copy.deepcopy(request)
|
||||
assert other is not request
|
||||
assert len(other.jobs) == len(request.jobs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"Action",
|
||||
[
|
||||
|
@ -215,6 +240,7 @@ def test_ProblemsGraph():
|
|||
|
||||
assert isinstance(outcome, libmambapy.solver.libsolv.UnSolvable)
|
||||
pbg = outcome.problems_graph(db)
|
||||
assert isinstance(pbg.root_node(), int)
|
||||
|
||||
# ProblemsGraph conflicts
|
||||
conflicts = pbg.conflicts()
|
||||
|
@ -239,3 +265,51 @@ def test_ProblemsGraph():
|
|||
nodes, edges = pbg.graph()
|
||||
assert len(nodes) > 0
|
||||
assert len(edges) > 0
|
||||
|
||||
# Simplify conflicts
|
||||
pbg = pbg.simplify_conflicts(pbg)
|
||||
|
||||
# CompressedProblemsGraph
|
||||
cp_pbg = libmambapy.solver.CompressedProblemsGraph.from_problems_graph(pbg)
|
||||
|
||||
assert isinstance(cp_pbg.root_node(), int)
|
||||
assert len(cp_pbg.conflicts()) == 2
|
||||
nodes, edges = cp_pbg.graph()
|
||||
assert len(nodes) > 0
|
||||
assert len(edges) > 0
|
||||
assert "is not installable" in cp_pbg.tree_message()
|
||||
|
||||
|
||||
def test_CompressedProblemsGraph_NamedList():
|
||||
ProblemsGraph = libmambapy.solver.ProblemsGraph
|
||||
CompressedProblemsGraph = libmambapy.solver.CompressedProblemsGraph
|
||||
PackageInfo = libmambapy.specs.PackageInfo
|
||||
|
||||
named_list = CompressedProblemsGraph.PackageListNode()
|
||||
assert len(named_list) == 0
|
||||
assert not named_list
|
||||
|
||||
# Add
|
||||
for ver, bld in [("1.0", "bld1"), ("2.0", "bld2"), ("3.0", "bld3"), ("4.0", "bld4")]:
|
||||
named_list.add(ProblemsGraph.PackageNode(PackageInfo("a", version=ver, build_string=bld)))
|
||||
|
||||
# Enumeration
|
||||
assert len(named_list) == 4
|
||||
assert named_list
|
||||
assert len(list(named_list)) == len(named_list)
|
||||
|
||||
# Methods
|
||||
assert named_list.name() == "a"
|
||||
list_str, count = named_list.versions_trunc(sep=":", etc="*", threshold=2)
|
||||
assert count == 4
|
||||
assert list_str == "1.0:2.0:*:4.0"
|
||||
list_str, count = named_list.build_strings_trunc(sep=":", etc="*", threshold=2)
|
||||
assert count == 4
|
||||
assert list_str == "bld1:bld2:*:bld4"
|
||||
list_str, count = named_list.versions_and_build_strings_trunc(sep=":", etc="*", threshold=2)
|
||||
assert count == 4
|
||||
assert list_str == "1.0 bld1:2.0 bld2:*:4.0 bld4"
|
||||
|
||||
# Clear
|
||||
named_list.clear()
|
||||
assert len(named_list) == 0
|
||||
|
|
Loading…
Reference in New Issue