From 6235ec9adb1e1c269c3a3a46cc7ba223897ff0cd Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 24 Aug 2023 14:43:32 +0200 Subject: [PATCH 01/48] start branch From 5114824f144b91f56332ce0a5b818b4abb8df4ae Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 24 Aug 2023 14:44:03 +0200 Subject: [PATCH 02/48] trigger ci --- conda_libmamba_solver/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conda_libmamba_solver/__init__.py b/conda_libmamba_solver/__init__.py index 6f6a4944..cd2d437d 100644 --- a/conda_libmamba_solver/__init__.py +++ b/conda_libmamba_solver/__init__.py @@ -29,3 +29,6 @@ def get_solver_class(key="libmamba"): ) return LibMambaSolver raise ValueError("Key must be 'libmamba'") + + +# TRIGGER CI; REMOVE From 9fe2743c5adec2882169095c7c6de8f5ffcfa1da Mon Sep 17 00:00:00 2001 From: jaimergp Date: Fri, 25 Aug 2023 17:44:41 +0200 Subject: [PATCH 03/48] port logic from conda/conda#9614 --- conda_libmamba_solver/state.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index a933de2f..9e317ab4 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -198,6 +198,8 @@ def __init__( self._update_modifier = self._default_to_context_if_null( "update_modifier", update_modifier ) + if prune and self._update_modifier == UpdateModifier.FREEZE_INSTALLED: + self._update_modifier = UpdateModifier.UPDATE_SPECS # revert to default self._deps_modifier = self._default_to_context_if_null("deps_modifier", deps_modifier) self._ignore_pinned = self._default_to_context_if_null("ignore_pinned", ignore_pinned) self._force_remove = self._default_to_context_if_null("force_remove", force_remove) @@ -521,8 +523,10 @@ def _initialize_specs_from_input_state(self): """ # Initialize specs following conda.core.solve._collect_all_metadata() - # First initialization depends on whether we have a history to work with or not - if self.solver_input_state.history: + if self.solver_input_state.prune: + pass # we do not initialize specs with history OR installed pkgs if we are pruning + # Otherwise, initialization depends on whether we have a history to work with or not + elif self.solver_input_state.history: # add in historically-requested specs self.specs.update(self.solver_input_state.history, reason="As in history") for name, record in self.solver_input_state.installed.items(): @@ -555,7 +559,7 @@ def _initialize_specs_from_input_state(self): # add everything in prefix if we have no history to work with (e.g. with --update-all) self.specs.update( {name: MatchSpec(name) for name in self.solver_input_state.installed}, - reason="Installed and no history available", + reason="Installed and no history available (prune=false)", ) # Add virtual packages so they are taken into account by the solver @@ -664,7 +668,7 @@ def _prepare_for_add(self, index: IndexHelper): ) else: # every other spec that matches something installed will be configured with - # only a target This is the case for conflicts, among others + # only a target. This is the case for conflicts, among others self.specs.set( name, MatchSpec(name, target=record.dist_str()), reason="Spec matches record" ) From 19c24fee8468af76910610369ce1e2d24c63b999 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Fri, 25 Aug 2023 17:44:55 +0200 Subject: [PATCH 04/48] deselect a couple tests (accelerate and libmamba do not get along) --- .../collect_upstream_conda_tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py index ea809f2f..88ff5e01 100644 --- a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py +++ b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py @@ -25,6 +25,10 @@ # Features / nomkl involved "test_features_solve_1", "test_prune_1", + "test_update_prune_2", + "test_update_prune_3", + # Message expected, but libmamba does not report constraints + "test_update_prune_5", # TODO: These ones need further investigation "test_channel_priority_churn_minimized", "test_priority_1", From cb3dc80d6a7a7056a4f077767b0448aead4ec030 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Fri, 25 Aug 2023 17:53:12 +0200 Subject: [PATCH 05/48] add news --- news/267-prune | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 news/267-prune diff --git a/news/267-prune b/news/267-prune new file mode 100644 index 00000000..2e0453ee --- /dev/null +++ b/news/267-prune @@ -0,0 +1,21 @@ +### Enhancements + +* + +### Bug fixes + +* Port logic from [conda/conda#9614](https://github.com/conda/conda/pull/9614), which fixes + a bug where the `--prune` flag was not working correctly in `conda env update` commands. + (#267) + +### Deprecations + +* + +### Docs + +* + +### Other + +* From 1c385eb92d668864b1f82c9bf6e77df03659a89e Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 28 Aug 2023 14:49:27 +0200 Subject: [PATCH 06/48] Apply suggestions from code review --- conda_libmamba_solver/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/conda_libmamba_solver/__init__.py b/conda_libmamba_solver/__init__.py index cd2d437d..6f6a4944 100644 --- a/conda_libmamba_solver/__init__.py +++ b/conda_libmamba_solver/__init__.py @@ -29,6 +29,3 @@ def get_solver_class(key="libmamba"): ) return LibMambaSolver raise ValueError("Key must be 'libmamba'") - - -# TRIGGER CI; REMOVE From 14061ed19dccc9394cf2922a594809854e5a5437 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 28 Aug 2023 16:09:24 +0200 Subject: [PATCH 07/48] Alternative logic for solver creation tasks --- conda_libmamba_solver/mamba_utils.py | 2 + conda_libmamba_solver/solver.py | 149 ++++++++++----------------- tests/test_solvers.py | 37 ++++++- tests/utils.py | 2 +- 4 files changed, 93 insertions(+), 97 deletions(-) diff --git a/conda_libmamba_solver/mamba_utils.py b/conda_libmamba_solver/mamba_utils.py index d6891394..2ab87976 100644 --- a/conda_libmamba_solver/mamba_utils.py +++ b/conda_libmamba_solver/mamba_utils.py @@ -9,6 +9,7 @@ # 2022.11.14: only keeping channel prioritization and context initialization logic now import logging +from functools import lru_cache from importlib.metadata import version from typing import Dict @@ -19,6 +20,7 @@ log = logging.getLogger(f"conda.{__name__}") +@lru_cache(maxsize=1) def mamba_version(): return version("libmambapy") diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 2b8279a8..6162905e 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -403,16 +403,20 @@ def _spec_to_str(spec): return str(spec) def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutputState): - # These packages receive special protection, since they will be - # exempt from conflict treatment (ALLOWUNINSTALL) and if installed - # their updates will be considered ESSENTIAL and USERINSTALLED - protected = ( - ["python", "conda"] - + list(in_state.history.keys()) - + list(in_state.aggressive_updates.keys()) - ) + tasks = defaultdict(list) - # Fast-track python version changes + # Protect history and aggressive updates from being uninstalled if possible. From libsolv + # docs: "The matching installed packages are considered to be installed by a user, thus not + # installed to fulfill some dependency. This is needed input for the calculation of + # unneeded packages for jobs that have the SOLVER_CLEANDEPS flag set." + tasks[("USERINSTALLED", api.SOLVER_USERINSTALLED)] += [ + *in_state.history, + *in_state.aggressive_updates, + *in_state.pinned, + *in_state.do_not_remove, + ] + + # Fast-track python version changes (Part 1/2) # ## When the Python version changes, this implies all packages depending on # ## python will be reinstalled too. This can mean that we'll have to try for every # ## installed package to result in a conflict before we get to actually solve everything @@ -423,96 +427,32 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu to_be_installed_python = out_state.specs.get("python") if installed_python and to_be_installed_python: python_version_might_change = not to_be_installed_python.match(installed_python) - - tasks = defaultdict(list) + + # Add specs to install for name, spec in out_state.specs.items(): if name.startswith("__"): - continue + continue # ignore virtual packages spec = self._check_spec_compat(spec) spec_str = self._spec_to_str(spec) installed = in_state.installed.get(name) - - key = "INSTALL", api.SOLVER_INSTALL - - # Fast-track Python version changes: mark non-noarch Python-depending packages as - # conflicting (see `python_version_might_change` definition above for more details) - if python_version_might_change and installed is not None: - if installed.noarch is not None: - continue - for dep in installed.depends: - dep_spec = MatchSpec(dep) - if dep_spec.name in ("python", "python_abi"): - reason = "Python version might change and this package depends on Python" - out_state.conflicts.update( - {name: spec}, - reason=reason, - overwrite=False, - ) - break - - # ## Low-prio task ### - if name in out_state.conflicts and name not in protected: - tasks[("DISFAVOR", api.SOLVER_DISFAVOR)].append(spec_str) - tasks[("ALLOWUNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(spec_str) - - if installed is not None: - # ## Regular task ### - key = "UPDATE", api.SOLVER_UPDATE - - # ## Protect if installed AND history - if name in protected: - installed_spec = self._spec_to_str(installed.to_match_spec()) - tasks[("USERINSTALLED", api.SOLVER_USERINSTALLED)].append(installed_spec) - # This is "just" an essential job, so it gets higher priority in the solver - # conflict resolution. We do this because these are "protected" packages - # (history, aggressive updates) that we should try not messing with if - # conflicts appear - key = ("UPDATE | ESSENTIAL", api.SOLVER_UPDATE | api.SOLVER_ESSENTIAL) - - # ## Here we deal with the "bare spec update" problem - # ## I am only adding this for legacy / test compliancy reasons; forced updates - # ## like this should (imo) use constrained specs (e.g. conda install python=3) - # ## or the update command as in `conda update python`. however conda thinks - # ## differently of update vs install (quite counterintuitive): - # ## https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/installing-with-conda.html#conda-update-versus-conda-install # noqa - # ## this is tested in: - # ## tests/core/test_solve.py::test_pinned_1 - # ## tests/test_create.py::IntegrationTests::test_update_with_pinned_packages - # ## fixing this changes the outcome in other tests! - # let's say we have an environment with python 2.6 and we say `conda install - # python` libsolv will say we already have python and there's no reason to do - # anything else even if we force an update with essential, other packages in the - # environment (built for py26) will keep it in place. we offer two ways to deal - # with this libsolv behaviour issue: - # A) introduce an artificial version spec `python !=` - # B) use FORCEBEST -- this would be ideal, but sometimes in gets in the way, - # so we only use it as a last attempt effort. - # NOTE: This is a dirty-ish workaround... rethink? - requested = in_state.requested.get(name) - conditions = ( - requested, - spec == requested, - spec.strictness == 1, - self._command in ("update", "update+last_solve_attempt", None, NULL), - in_state.deps_modifier != DepsModifier.ONLY_DEPS, - in_state.update_modifier - not in (UpdateModifier.UPDATE_DEPS, UpdateModifier.FREEZE_INSTALLED), - ) - if all(conditions): - if "last_solve_attempt" in str(self._command): - key = ( - "UPDATE | ESSENTIAL | FORCEBEST", - api.SOLVER_UPDATE | api.SOLVER_ESSENTIAL | api.SOLVER_FORCEBEST, - ) - else: - # NOTE: This is ugly and there should be another way - spec_str = self._spec_to_str( - MatchSpec(spec, version=f">{installed.version}") - ) - - tasks[key].append(spec_str) - + requested = in_state.requested.get(name) + history = in_state.history.get(name) + pinned = in_state.pinned.get(name) + conflicting = out_state.conflicts.get(name) + + if requested: + if installed: + tasks[("UPDATE", api.SOLVER_UPDATE)].append(spec_str) + else: + tasks[("INSTALL", api.SOLVER_INSTALL)].append(spec_str) + elif pinned: + tasks[("ADD_PIN", api.SOLVER_NOOP)].append(spec_str) + elif not conflicting and installed: + tasks[("LOCK", api.SOLVER_LOCK)].append(name) + return dict(tasks) + + def _specs_to_tasks_remove(self, in_state: SolverInputState, out_state: SolverOutputState): # TODO: Consider merging add/remove in a single logic this so there's no split @@ -520,6 +460,7 @@ def _specs_to_tasks_remove(self, in_state: SolverInputState, out_state: SolverOu tasks = defaultdict(list) # Protect history and aggressive updates from being uninstalled if possible + for name, record in out_state.records.items(): if name in in_state.history or name in in_state.aggressive_updates: # MatchSpecs constructed from PackageRecords get parsed too @@ -598,7 +539,13 @@ def _parse_problems(cls, problems: str) -> Mapping[str, MatchSpec]: """ conflicts = [] not_found = [] - for line in problems.splitlines(): + if "1.4.5" <= mamba_version() < "1.5.0": + # 1.4.5 had a regression where it would return + # a single line with all the problems; fixed in 1.5.0 + problem_lines = [f" - {problem}" for problem in problems.split(" - ")[1:]] + else: + problem_lines = problems.splitlines()[1:] + for line in problem_lines: line = line.strip() words = line.split() if not line.startswith("- "): @@ -615,6 +562,12 @@ def _parse_problems(cls, problems: str) -> Mapping[str, MatchSpec]: conflicts.append(cls._str_to_matchspec(words[-1])) start = 3 if marker == 4 else 4 not_found.append(cls._str_to_matchspec(words[start:marker])) + elif "has constraint" in line and "conflicting with" in line: + # package libzlib-1.2.11-h4e544f5_1014 has constraint zlib 1.2.11 *_1014 + # conflicting with zlib-1.2.13-h998d150_0 + conflicts.append(cls._str_to_matchspec(words[-1])) + else: + log.debug("! Problem line not recognized: %s", line) return { "conflicts": {s.name: s for s in conflicts}, @@ -673,6 +626,14 @@ def _prepare_problems_message(self): # This error makes 'explain_problems()' crash. Anticipate. log.info("Failed to explain problems. Unsupported request.") return legacy_errors + if ( + mamba_version() <= "1.4.1" + and "conflicting requests" in self.solver.all_problems_to_str() + ): + # This error makes 'explain_problems()' crash in libmamba <=1.4.1. + # Anticipate and return simpler error earlier. + log.info("Failed to explain problems. Conflicting requests.") + return legacy_errors try: explained_errors = self.solver.explain_problems() except Exception as exc: diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 5b95584b..5f966ca7 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -9,13 +9,15 @@ from subprocess import check_call, run from uuid import uuid4 -from conda.common.compat import on_win +import pytest +from conda.common.compat import on_win, on_linux from conda.core.prefix_data import PrefixData, get_python_version_for_prefix from conda.testing.integration import Commands, make_temp_env, run_command from conda.testing.solver_helpers import SolverTests - from conda_libmamba_solver import LibMambaSolver +from .utils import conda_subprocess + class TestLibMambaSolver(SolverTests): @property @@ -197,3 +199,34 @@ def test_update_from_latest_not_downgrade(tmpdir): ) update_python = PrefixData(prefix).get("python") assert original_python.version == update_python.version + + +@pytest.mark.skipif(not on_linux, reason="Linux only") +def test_too_aggressive_update_to_conda_forge_packages(): + """ + Comes from report in https://github.com/conda/conda-libmamba-solver/issues/240 + We expect a minimum change to the 'base' environment if we only ask for a single package. + conda classic would just change a few (<5) packages, but libmamba seemed to upgrade + EVERYTHING it can to conda-forge. + """ + with make_temp_env("conda", "python", "--override-channels", "--channel=defaults") as prefix: + cmd = ( + "install", + "-p", + prefix, + "-c", + "conda-forge", + "libzlib", + "--json", + "--dry-run", + "-y", + "-vvv", + ) + env = os.environ.copy() + env.pop("CONDA_SOLVER", None) + p_classic = conda_subprocess(*cmd, "--solver=classic", explain=True, env=env) + p_libmamba = conda_subprocess(*cmd, "--solver=libmamba", explain=True, env=env) + data_classic = json.loads(p_classic.stdout) + data_libmamba = json.loads(p_libmamba.stdout) + assert len(data_classic["actions"]["LINK"]) < 10 + assert len(data_libmamba["actions"]["LINK"]) < 10 diff --git a/tests/utils.py b/tests/utils.py index 5ace7629..179f002f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -18,7 +18,7 @@ def conda_subprocess(*args, explain=False, capture_output=True, **kwargs) -> Com text=kwargs.pop("text", capture_output), **kwargs, ) - if capture_output and p.returncode: + if capture_output and (explain or p.returncode): print(p.stdout) print(p.stderr, file=sys.stderr) p.check_returncode() From ea643798185c31fad41c2c869e74a1cdad7760db Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:14:17 +0000 Subject: [PATCH 08/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- conda_libmamba_solver/repoquery.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conda_libmamba_solver/repoquery.py b/conda_libmamba_solver/repoquery.py index 3a864aeb..5a2178a8 100644 --- a/conda_libmamba_solver/repoquery.py +++ b/conda_libmamba_solver/repoquery.py @@ -52,7 +52,9 @@ def configure_parser(parser: argparse.ArgumentParser): view_grp.add_argument( "-t", "--tree", action="store_true", help="Show dependencies in a tree-like format." ) - view_grp.add_argument("--recursive", action="store_true", help="Show dependencies recursively.") + view_grp.add_argument( + "--recursive", action="store_true", help="Show dependencies recursively." + ) subparser = parser.add_subparsers(dest="subcmd") From fc1530c30c852f347def1f53628f4b801ab80276 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 12:24:25 +0200 Subject: [PATCH 09/48] fix tests/core/test_solve.py::test_auto_update_conda --- conda_libmamba_solver/state.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index a933de2f..0073b8a2 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -280,6 +280,8 @@ def aggressive_updates(self) -> Mapping[str, MatchSpec]: associated version or build constrain. Note that the packages here returned do not need to be installed. """ + if context.auto_update_conda and paths_equal(self.prefix, context.root_prefix): + return MappingProxyType({"conda": MatchSpec("conda"), **self._aggressive_updates}) return MappingProxyType(self._aggressive_updates) @property From 15486b028eb2101e508c191dc6331a745fffaefa Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 12:24:53 +0200 Subject: [PATCH 10/48] some progress with pins (but not quite there yet) --- conda_libmamba_solver/solver.py | 64 +++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 6162905e..1c1dad23 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -364,7 +364,13 @@ def _solve_attempt( tasks = self._specs_to_tasks(in_state, out_state) for (task_name, task_type), specs in tasks.items(): log.debug("Adding task %s with specs %s", task_name, specs) - self.solver.add_jobs(specs, task_type) + print("Adding task %s with specs %s", task_name, specs) + if task_name == "ADD_PIN": + # ## Add pins + for spec in specs: + self.solver.add_pin(spec) + else: + self.solver.add_jobs(specs, task_type) # ## Run solver solved = self.solver.solve() @@ -409,12 +415,12 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu # docs: "The matching installed packages are considered to be installed by a user, thus not # installed to fulfill some dependency. This is needed input for the calculation of # unneeded packages for jobs that have the SOLVER_CLEANDEPS flag set." - tasks[("USERINSTALLED", api.SOLVER_USERINSTALLED)] += [ + user_installed = { *in_state.history, *in_state.aggressive_updates, *in_state.pinned, *in_state.do_not_remove, - ] + } # Fast-track python version changes (Part 1/2) # ## When the Python version changes, this implies all packages depending on @@ -432,27 +438,49 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu for name, spec in out_state.specs.items(): if name.startswith("__"): continue # ignore virtual packages - spec = self._check_spec_compat(spec) + spec: MatchSpec = self._check_spec_compat(spec) spec_str = self._spec_to_str(spec) - installed = in_state.installed.get(name) - requested = in_state.requested.get(name) - history = in_state.history.get(name) - pinned = in_state.pinned.get(name) - conflicting = out_state.conflicts.get(name) - + installed: PackageRecord = in_state.installed.get(name) + requested: MatchSpec = in_state.requested.get(name) + history: MatchSpec = in_state.history.get(name) + pinned: MatchSpec = in_state.pinned.get(name) + conflicting: MatchSpec = out_state.conflicts.get(name) + + if name in user_installed and installed: + installed_spec_str = self._spec_to_str(installed.to_match_spec()) + tasks[("USERINSTALLED", api.SOLVER_USERINSTALLED)].append(installed_spec_str) + + # These specs are explicit in some sort of way if requested: - if installed: - tasks[("UPDATE", api.SOLVER_UPDATE)].append(spec_str) + if self._command in ("install", "create", None, NULL): + tasks[("INSTALL", api.SOLVER_INSTALL)].append(self._spec_to_str(requested)) else: - tasks[("INSTALL", api.SOLVER_INSTALL)].append(spec_str) + tasks[("UPDATE", api.SOLVER_UPDATE)].append(self._spec_to_str(requested)) + elif installed and name in in_state.aggressive_updates: + tasks[("UPDATE", api.SOLVER_UPDATE)].append(name) elif pinned: - tasks[("ADD_PIN", api.SOLVER_NOOP)].append(spec_str) - elif not conflicting and installed: + tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(pinned)) + # if installed: + # # TODO: Pins should worrrrk + # tasks[("UPDATE", api.SOLVER_UPDATE)].append(self._spec_to_str(pinned)) + # These specs are "implicit"; the solver logic massages them for better UX + # as long as they don't cause trouble + elif name == "python" and installed: + pyver = ".".join(installed.version.split(".")[:2]) + tasks[("ADD_PIN", api.SOLVER_NOOP)].append(f"python {pyver}.*") + # We shouldn't need this and will cause unneeded updates, but the pins + # are not working for some reason (https://github.com/mamba-org/mamba/issues/2737) + # tasks[("UPDATE", api.SOLVER_UPDATE)].append(f"python {pyver}.*") + elif history: + tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(history)) + # if installed: + # # TODO: Pins should worrrrk + # tasks[("UPDATE", api.SOLVER_UPDATE)].append(self._spec_to_str(history)) + elif not conflicting and installed: # we freeze everything else as installed tasks[("LOCK", api.SOLVER_LOCK)].append(name) - + # pass + return dict(tasks) - - def _specs_to_tasks_remove(self, in_state: SolverInputState, out_state: SolverOutputState): # TODO: Consider merging add/remove in a single logic this so there's no split From 889fe15e8753dfd3701efcfec9ff07380b2ad641 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 14:23:26 +0200 Subject: [PATCH 11/48] Increase timeouts in CI env setup --- .github/workflows/upstream_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/upstream_tests.yml b/.github/workflows/upstream_tests.yml index 59c08cf2..6d780eb4 100644 --- a/.github/workflows/upstream_tests.yml +++ b/.github/workflows/upstream_tests.yml @@ -438,7 +438,7 @@ jobs: - name: Setup environment working-directory: conda # CONDA-LIBMAMBA-SOLVER CHANGE shell: bash -el {0} - timeout-minutes: 10 + timeout-minutes: 15 run: | # CONDA-LIBMAMBA-SOLVER CHANGE cat ../conda-libmamba-solver/dev/requirements.txt >> tests/requirements.txt From e40f0da3c96651312fa6dc765aaf259836ff12df Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 17:26:30 +0200 Subject: [PATCH 12/48] allow uninstall by default only on removals --- conda_libmamba_solver/solver.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index c697fce6..f68d69ba 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -326,13 +326,14 @@ def _log_info(self): def _setup_solver(self, index: LibMambaIndexHelper): self._solver_options = solver_options = [ (api.SOLVER_FLAG_ALLOW_DOWNGRADE, 1), - (api.SOLVER_FLAG_ALLOW_UNINSTALL, 1), (api.SOLVER_FLAG_INSTALL_ALSO_UPDATES, 1), - (api.SOLVER_FLAG_FOCUS_BEST, 1), - (api.SOLVER_FLAG_BEST_OBEY_POLICY, 1), + # (api.SOLVER_FLAG_FOCUS_BEST, 1), + # (api.SOLVER_FLAG_BEST_OBEY_POLICY, 1), ] if context.channel_priority is ChannelPriority.STRICT: solver_options.append((api.SOLVER_FLAG_STRICT_REPO_PRIORITY, 1)) + if self.specs_to_remove and self._command in ("remove", None, NULL): + solver_options.append((api.SOLVER_FLAG_ALLOW_UNINSTALL, 1)) self.solver = api.Solver(index._pool, self._solver_options) From 3073a78dd0102dc6523b3f1629adff4c2153d926 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 17:27:07 +0200 Subject: [PATCH 13/48] handle prune --- conda_libmamba_solver/solver.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index f68d69ba..29df1e51 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -463,31 +463,24 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu pinned: MatchSpec = in_state.pinned.get(name) conflicting: MatchSpec = out_state.conflicts.get(name) - if name in user_installed and installed: + if name in user_installed and installed and not in_state.prune: installed_spec_str = self._spec_to_str(installed.to_match_spec()) tasks[("USERINSTALLED", api.SOLVER_USERINSTALLED)].append(installed_spec_str) # These specs are explicit in some sort of way if requested: - if self._command in ("install", "create", None, NULL): - tasks[("INSTALL", api.SOLVER_INSTALL)].append(self._spec_to_str(requested)) - else: - tasks[("UPDATE", api.SOLVER_UPDATE)].append(self._spec_to_str(requested)) + tasks[("INSTALL", api.SOLVER_INSTALL)].append(self._spec_to_str(requested)) elif installed and name in in_state.aggressive_updates: tasks[("UPDATE", api.SOLVER_UPDATE)].append(name) elif pinned: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(pinned)) - # if installed: - # # TODO: Pins should worrrrk - # tasks[("UPDATE", api.SOLVER_UPDATE)].append(self._spec_to_str(pinned)) # These specs are "implicit"; the solver logic massages them for better UX # as long as they don't cause trouble + elif in_state.prune: + continue elif name == "python" and installed: pyver = ".".join(installed.version.split(".")[:2]) tasks[("ADD_PIN", api.SOLVER_NOOP)].append(f"python {pyver}.*") - # We shouldn't need this and will cause unneeded updates, but the pins - # are not working for some reason (https://github.com/mamba-org/mamba/issues/2737) - # tasks[("UPDATE", api.SOLVER_UPDATE)].append(f"python {pyver}.*") elif history: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(history)) # if installed: From 49f201a46c97c127552a0d43f86aa2e85e4a4a8d Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 17:27:23 +0200 Subject: [PATCH 14/48] avoid crash --- conda_libmamba_solver/solver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 29df1e51..e36a1096 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -660,6 +660,8 @@ def _maybe_raise_for_problems( def _prepare_problems_message(self): legacy_errors = self.solver.problems_to_str() + if not " - " in legacy_errors: + return "Failed with empty error message" if "unsupported request" in legacy_errors: # This error makes 'explain_problems()' crash. Anticipate. log.info("Failed to explain problems. Unsupported request.") From c5cdb047edf9b4e64e313f1fafe12bd5fc12af0f Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 17:36:16 +0200 Subject: [PATCH 15/48] cleanup --- conda_libmamba_solver/solver.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index e36a1096..18514cf6 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -365,7 +365,6 @@ def _solve_attempt( tasks = self._specs_to_tasks(in_state, out_state) for (task_name, task_type), specs in tasks.items(): log.debug("Adding task %s with specs %s", task_name, specs) - print("Adding task %s with specs %s", task_name, specs) if task_name == "ADD_PIN": # ## Add pins for spec in specs: @@ -483,12 +482,8 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu tasks[("ADD_PIN", api.SOLVER_NOOP)].append(f"python {pyver}.*") elif history: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(history)) - # if installed: - # # TODO: Pins should worrrrk - # tasks[("UPDATE", api.SOLVER_UPDATE)].append(self._spec_to_str(history)) elif not conflicting and installed: # we freeze everything else as installed tasks[("LOCK", api.SOLVER_LOCK)].append(name) - # pass return dict(tasks) From e5b3e75a8cb7701ac893fd43840f8828d8c9205f Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 18:21:05 +0200 Subject: [PATCH 16/48] only lock the installed spec --- conda_libmamba_solver/solver.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 18514cf6..7831fb05 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -480,10 +480,17 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu elif name == "python" and installed: pyver = ".".join(installed.version.split(".")[:2]) tasks[("ADD_PIN", api.SOLVER_NOOP)].append(f"python {pyver}.*") - elif history: + elif history and not history.is_name_only_spec and not conflicting: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(history)) elif not conflicting and installed: # we freeze everything else as installed - tasks[("LOCK", api.SOLVER_LOCK)].append(name) + lock = True + if python_version_might_change and installed.noarch is None: + for dep in installed.depends: + if MatchSpec(dep).name in ("python", "python_abi"): + lock = False + break + if lock: + tasks[("LOCK", api.SOLVER_LOCK)].append(self._spec_to_str(installed.to_match_spec())) return dict(tasks) From 23958602937ba4557fe5fc7d58d1c29b4d17bbbf Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 18:25:23 +0200 Subject: [PATCH 17/48] pre-commit --- conda_libmamba_solver/solver.py | 25 ++++++++++--------------- tests/test_solvers.py | 3 ++- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 7831fb05..c4d12cae 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -20,14 +20,7 @@ import libmambapy as api from boltons.setutils import IndexedSet from conda import __version__ as _conda_version -from conda.base.constants import ( - REPODATA_FN, - UNKNOWN_CHANNEL, - ChannelPriority, - DepsModifier, - UpdateModifier, - on_win, -) +from conda.base.constants import REPODATA_FN, UNKNOWN_CHANNEL, ChannelPriority, on_win from conda.base.context import context from conda.common.constants import NULL from conda.common.io import Spinner @@ -437,7 +430,7 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu *in_state.pinned, *in_state.do_not_remove, } - + # Fast-track python version changes (Part 1/2) # ## When the Python version changes, this implies all packages depending on # ## python will be reinstalled too. This can mean that we'll have to try for every @@ -449,13 +442,12 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu to_be_installed_python = out_state.specs.get("python") if installed_python and to_be_installed_python: python_version_might_change = not to_be_installed_python.match(installed_python) - + # Add specs to install for name, spec in out_state.specs.items(): if name.startswith("__"): continue # ignore virtual packages spec: MatchSpec = self._check_spec_compat(spec) - spec_str = self._spec_to_str(spec) installed: PackageRecord = in_state.installed.get(name) requested: MatchSpec = in_state.requested.get(name) history: MatchSpec = in_state.history.get(name) @@ -465,7 +457,7 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu if name in user_installed and installed and not in_state.prune: installed_spec_str = self._spec_to_str(installed.to_match_spec()) tasks[("USERINSTALLED", api.SOLVER_USERINSTALLED)].append(installed_spec_str) - + # These specs are explicit in some sort of way if requested: tasks[("INSTALL", api.SOLVER_INSTALL)].append(self._spec_to_str(requested)) @@ -490,7 +482,9 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu lock = False break if lock: - tasks[("LOCK", api.SOLVER_LOCK)].append(self._spec_to_str(installed.to_match_spec())) + tasks[("LOCK", api.SOLVER_LOCK)].append( + self._spec_to_str(installed.to_match_spec()) + ) return dict(tasks) @@ -662,8 +656,9 @@ def _maybe_raise_for_problems( def _prepare_problems_message(self): legacy_errors = self.solver.problems_to_str() - if not " - " in legacy_errors: - return "Failed with empty error message" + if " - " not in legacy_errors: + # This makes 'explain_problems()' crash. Anticipate. + return "Failed with empty error message." if "unsupported request" in legacy_errors: # This error makes 'explain_problems()' crash. Anticipate. log.info("Failed to explain problems. Unsupported request.") diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 74b90fd8..642796e6 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -10,10 +10,11 @@ from uuid import uuid4 import pytest -from conda.common.compat import on_win, on_linux +from conda.common.compat import on_linux, on_win from conda.core.prefix_data import PrefixData, get_python_version_for_prefix from conda.testing.integration import Commands, make_temp_env, run_command from conda.testing.solver_helpers import SolverTests + from conda_libmamba_solver import LibMambaSolver from conda_libmamba_solver.mamba_utils import mamba_version From ebc28859a7786de029cc0ad4b4d73beabaf9e007 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 21:00:04 +0200 Subject: [PATCH 18/48] add SolverInputState.always_update helper mapping --- conda_libmamba_solver/state.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index ba9b3389..c2feea29 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -282,10 +282,26 @@ def aggressive_updates(self) -> Mapping[str, MatchSpec]: associated version or build constrain. Note that the packages here returned do not need to be installed. """ - if context.auto_update_conda and paths_equal(self.prefix, context.root_prefix): - return MappingProxyType({"conda": MatchSpec("conda"), **self._aggressive_updates}) return MappingProxyType(self._aggressive_updates) + @property + def always_update(self) -> Mapping[str, MatchSpec]: + """ + Merged lists of packages that should always be updated, depending on the flags, including: + - aggressive_updates + - conda if auto_update_conda is true and we are on the base env + - almost all packages if update_all is true + - etc + """ + pkgs = {**self._aggressive_updates} + if context.auto_update_conda and paths_equal(self.prefix, context.root_prefix): + pkgs.setdefault("conda", MatchSpec("conda")) + if self.update_modifier.UPDATE_ALL: + for pkg in self.installed: + if pkg != "python": + pkgs.setdefault(pkg, MatchSpec(pkg)) + return MappingProxyType(pkgs) + @property def do_not_remove(self) -> Mapping[str, MatchSpec]: """ From ba202e10cb3dea60dedf04a4ffd7387eec96597b Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 21:02:02 +0200 Subject: [PATCH 19/48] allow uninstall of conflicts --- conda_libmamba_solver/solver.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index c4d12cae..4626e89a 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -474,17 +474,19 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu tasks[("ADD_PIN", api.SOLVER_NOOP)].append(f"python {pyver}.*") elif history and not history.is_name_only_spec and not conflicting: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(history)) - elif not conflicting and installed: # we freeze everything else as installed - lock = True - if python_version_might_change and installed.noarch is None: - for dep in installed.depends: - if MatchSpec(dep).name in ("python", "python_abi"): - lock = False - break - if lock: - tasks[("LOCK", api.SOLVER_LOCK)].append( - self._spec_to_str(installed.to_match_spec()) - ) + elif installed: + if conflicting: + tasks[("ALLOW_UNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(name) + else: + # we freeze everything else as installed + lock = True + if python_version_might_change and installed.noarch is None: + for dep in installed.depends: + if MatchSpec(dep).name in ("python", "python_abi"): + lock = False + break + if lock: + tasks[("LOCK", api.SOLVER_LOCK)].append(installed_spec_str) return dict(tasks) From 10a67f8d876ab01dd440156df0031d37784e7ec8 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 21:04:55 +0200 Subject: [PATCH 20/48] use always_update list for simplicity --- conda_libmamba_solver/solver.py | 21 ++++++++++++++------- conda_libmamba_solver/state.py | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 4626e89a..0b941555 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -425,10 +425,14 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu # installed to fulfill some dependency. This is needed input for the calculation of # unneeded packages for jobs that have the SOLVER_CLEANDEPS flag set." user_installed = { - *in_state.history, - *in_state.aggressive_updates, - *in_state.pinned, - *in_state.do_not_remove, + pkg + for pkg in ( + *in_state.history, + *in_state.aggressive_updates, + *in_state.pinned, + *in_state.do_not_remove + ) + if pkg in in_state.installed } # Fast-track python version changes (Part 1/2) @@ -449,19 +453,22 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu continue # ignore virtual packages spec: MatchSpec = self._check_spec_compat(spec) installed: PackageRecord = in_state.installed.get(name) + if installed: + installed_spec_str = self._spec_to_str(installed.to_match_spec()) + else: + installed_spec_str = None requested: MatchSpec = in_state.requested.get(name) history: MatchSpec = in_state.history.get(name) pinned: MatchSpec = in_state.pinned.get(name) conflicting: MatchSpec = out_state.conflicts.get(name) - if name in user_installed and installed and not in_state.prune: - installed_spec_str = self._spec_to_str(installed.to_match_spec()) + if name in user_installed and not in_state.prune and not conflicting: tasks[("USERINSTALLED", api.SOLVER_USERINSTALLED)].append(installed_spec_str) # These specs are explicit in some sort of way if requested: tasks[("INSTALL", api.SOLVER_INSTALL)].append(self._spec_to_str(requested)) - elif installed and name in in_state.aggressive_updates: + elif name in in_state.always_update: tasks[("UPDATE", api.SOLVER_UPDATE)].append(name) elif pinned: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(pinned)) diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index c2feea29..cb65e11a 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -293,7 +293,7 @@ def always_update(self) -> Mapping[str, MatchSpec]: - almost all packages if update_all is true - etc """ - pkgs = {**self._aggressive_updates} + pkgs = {pkg: MatchSpec(pkg) for pkg in self.aggressive_updates if pkg in self.installed} if context.auto_update_conda and paths_equal(self.prefix, context.root_prefix): pkgs.setdefault("conda", MatchSpec("conda")) if self.update_modifier.UPDATE_ALL: From 122f835ee644ef1a3febfe71ee0c2b77821051ae Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 21:27:46 +0200 Subject: [PATCH 21/48] not update-all if pinned --- conda_libmamba_solver/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index cb65e11a..c2328340 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -298,7 +298,7 @@ def always_update(self) -> Mapping[str, MatchSpec]: pkgs.setdefault("conda", MatchSpec("conda")) if self.update_modifier.UPDATE_ALL: for pkg in self.installed: - if pkg != "python": + if pkg != "python" and pkg not in self.pinned: pkgs.setdefault(pkg, MatchSpec(pkg)) return MappingProxyType(pkgs) From 92f3314077dc71c4bef022624445b1f342a9a5c3 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 22:22:57 +0200 Subject: [PATCH 22/48] explain pins if involved in errors --- conda_libmamba_solver/solver.py | 60 +++++++++++++++++++++++++++++---- conda_libmamba_solver/state.py | 7 ++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 0b941555..648acfd1 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -294,7 +294,7 @@ def _solving_loop( self._command += "+last_solve_attempt" solved = self._solve_attempt(in_state, out_state, index) if not solved: - message = self._prepare_problems_message() + message = self._prepare_problems_message(pins=out_state.pins) exc = LibMambaUnsatisfiableError(message) exc.allow_retry = False raise exc @@ -355,13 +355,17 @@ def _solve_attempt( log.debug("Computed specs: %s", out_state.specs) # ## Convert to tasks + out_state.pins.clear() + n_pins = 0 tasks = self._specs_to_tasks(in_state, out_state) for (task_name, task_type), specs in tasks.items(): log.debug("Adding task %s with specs %s", task_name, specs) if task_name == "ADD_PIN": # ## Add pins for spec in specs: + n_pins += 1 self.solver.add_pin(spec) + out_state.pins[f"pin-{n_pins}"] = spec continue try: @@ -390,7 +394,7 @@ def _solve_attempt( problems = self.solver.problems_to_str() old_conflicts = out_state.conflicts.copy() - new_conflicts = self._maybe_raise_for_problems(problems, old_conflicts) + new_conflicts = self._maybe_raise_for_problems(problems, old_conflicts, out_state.pins) log.debug("Attempt failed with %s conflicts", len(new_conflicts)) out_state.conflicts.update(new_conflicts.items(), reason="New conflict found") return False @@ -482,7 +486,7 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu elif history and not history.is_name_only_spec and not conflicting: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(history)) elif installed: - if conflicting: + if conflicting and not history: tasks[("ALLOW_UNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(name) else: # we freeze everything else as installed @@ -609,6 +613,12 @@ def _parse_problems(cls, problems: str) -> Mapping[str, MatchSpec]: # package libzlib-1.2.11-h4e544f5_1014 has constraint zlib 1.2.11 *_1014 # conflicting with zlib-1.2.13-h998d150_0 conflicts.append(cls._str_to_matchspec(words[-1])) + elif "cannot install both pin-" in line and "and pin-" in line: + # a pin is in conflict with another pin + pin_a = words[3].rsplit("-", 1)[0] + pin_b = words[5].rsplit("-", 1)[0] + conflicts.append(MatchSpec(pin_a)) + conflicts.append(MatchSpec(pin_b)) else: log.debug("! Problem line not recognized: %s", line) @@ -621,6 +631,7 @@ def _maybe_raise_for_problems( self, problems: Optional[Union[str, Mapping]] = None, previous_conflicts: Mapping[str, MatchSpec] = None, + pins: Mapping[str, MatchSpec] = None, ): if self.solver is None: raise RuntimeError("Solver is not initialized. Call `._setup_solver()` first.") @@ -656,14 +667,14 @@ def _maybe_raise_for_problems( if (previous and (previous_set == current_set)) or len(diff) >= 10: # We have same or more (up to 10) unsatisfiable now! Abort to avoid recursion - message = self._prepare_problems_message() + message = self._prepare_problems_message(pins=pins) exc = LibMambaUnsatisfiableError(message) # do not allow conda.cli.install to try more things exc.allow_retry = False raise exc return unsatisfiable - def _prepare_problems_message(self): + def _prepare_problems_message(self, pins=None): legacy_errors = self.solver.problems_to_str() if " - " not in legacy_errors: # This makes 'explain_problems()' crash. Anticipate. @@ -684,9 +695,44 @@ def _prepare_problems_message(self): explained_errors = self.solver.explain_problems() except Exception as exc: log.warning("Failed to explain problems", exc_info=exc) - return legacy_errors + return self._explain_with_pins(legacy_errors, pins) else: - return f"{legacy_errors}\n{explained_errors}" + msg = f"{legacy_errors}\n{explained_errors}" + return self._explain_with_pins(msg, pins) + + def _explain_with_pins(self, message, pins): + """ + Add info about pins to the error message. + This might be temporary as libmamba improves their error messages. + As of 1.5.0, if a pin introduces a conflict, it results in a cryptic error message like: + + ``` + Encountered problems while solving: + - cannot install both pin-1-1 and pin-1-1 + + Could not solve for environment specs + The following packages are incompatible + └─ pin-1 is installable with the potential options + ├─ pin-1 1, which can be installed; + └─ pin-1 1 conflicts with any installable versions previously reported. + ``` + + Since the pin-N name is masked, we add the following snippet underneath: + + ``` + Pins seem to be involved in the conflict. Currently pinned specs: + - python 2.7.* (labeled as 'pin-1') + + If Python is involved, try adding it explicitly to the command-line. + ``` + """ + if pins and "pin-1" in message: # add info about pins for easier debugging + pin_message = "Pins seem to be involved in the conflict. Currently pinned specs:\n" + for pin_name, spec in pins.items(): + pin_message += f" - {spec} (labeled as '{pin_name}')\n" + pin_message += "\nIf python is involved, try adding it explicitly to the command-line." + return f"{message}\n\n{pin_message}" + return message def _maybe_raise_for_conda_build( self, diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index c2328340..e2873943 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -463,6 +463,8 @@ class SolverOutputState: If a solve attempt is not successful, conflicting specs are kept here for further relaxation of the version and build constrains. If not provided, their default value is a blank mapping. + pins + Packages that ended up being pinned. Mostly used for reporting and debugging. Notes ----- @@ -496,6 +498,7 @@ def __init__( for_history: Optional[Mapping[str, MatchSpec]] = None, neutered: Optional[Mapping[str, MatchSpec]] = None, conflicts: Optional[Mapping[str, MatchSpec]] = None, + pins: Optional[Mapping[str, MatchSpec]] = None, ): self.solver_input_state: SolverInputState = solver_input_state @@ -534,6 +537,10 @@ def __init__( "conflicts", data=(conflicts or {}), reason="From arguments" ) + self.pins: Mapping[str, MatchSpec] = TrackedMap( + "pins", data=(pins or {}), reason="From arguments" + ) + def _initialize_specs_from_input_state(self): """ Provide the initial value for the ``.specs`` mapping. This depends on whether From 81d350d7c33856d19e6d632f1a00ca6acea3da9e Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 23:16:53 +0200 Subject: [PATCH 23/48] re-enable original test_neutering_of_historic_specs --- .../collect_upstream_conda_tests.py | 1 - tests/test_modified_upstream.py | 24 ------------------- 2 files changed, 25 deletions(-) diff --git a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py index e30f36b8..b0b63332 100644 --- a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py +++ b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py @@ -50,7 +50,6 @@ "test_conda_recovery_of_pip_inconsistent_env", # Known bug in mamba; see https://github.com/mamba-org/mamba/issues/1197 "test_offline_with_empty_index_cache", - "test_neutering_of_historic_specs", "test_pinned_override_with_explicit_spec", # TODO: Investigate why this fails on Windows now "test_install_update_deps_only_deps_flags", diff --git a/tests/test_modified_upstream.py b/tests/test_modified_upstream.py index 0651d594..3d7824bb 100644 --- a/tests/test_modified_upstream.py +++ b/tests/test_modified_upstream.py @@ -63,30 +63,6 @@ class PatchedCondaTestCreate(BaseTestCase): def setUp(self): PackageCacheData.clear() - # https://github.com/conda/conda/issues/9124 - @pytest.mark.skipif( - context.subdir != "linux-64", reason="lazy; package constraint here only valid on linux-64" - ) - def test_neutering_of_historic_specs(self): - with make_temp_env("psutil=5.6.3=py37h7b6447c_0") as prefix: - stdout, stderr, _ = run_command(Commands.INSTALL, prefix, "python=3.6") - with open(os.path.join(prefix, "conda-meta", "history")) as f: - d = f.read() - - ## MODIFIED - #  libmamba relaxes more aggressively sometimes - #  instead of relaxing from pkgname=version=build to pkgname=version, it - #  goes to just pkgname; this is because libmamba does not take into account - #  matchspec target and optionality (iow, MatchSpec.conda_build_form() does not) - #  Original check was stricter: - ### assert re.search(r"neutered specs:.*'psutil==5.6.3'\]", d) - assert re.search(r"neutered specs:.*'psutil'\]", d) - ## /MODIFIED - - # this would be unsatisfiable if the neutered specs were not being factored in correctly. - # If this command runs successfully (does not raise), then all is well. - stdout, stderr, _ = run_command(Commands.INSTALL, prefix, "imagesize") - def test_pinned_override_with_explicit_spec(self): with make_temp_env("python=3.6") as prefix: ## MODIFIED From 629f381c943e8caf4c3ec3aa7212c58052d49852 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 29 Aug 2023 23:22:13 +0200 Subject: [PATCH 24/48] bump minimum required conda and libmamba versions --- dev/requirements.txt | 6 +++--- pyproject.toml | 4 ++-- recipe/meta.yaml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dev/requirements.txt b/dev/requirements.txt index 89b06f37..dd616efb 100644 --- a/dev/requirements.txt +++ b/dev/requirements.txt @@ -2,8 +2,8 @@ pip # run-time boltons>=23.0.0 -conda>=23.3.1 -libmamba>=1.4.1 -libmambapy>=1.4.1 +conda>=23.7.3 +libmamba>=1.5.0 +libmambapy>=1.5.0 # be explicit about sqlite because sometimes it's removed from the env :shrug: sqlite diff --git a/pyproject.toml b/pyproject.toml index f108a4bf..5753e291 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,8 +23,8 @@ classifiers = [ ] requires-python = ">=3.8" dependencies = [ - "conda >=23.5.0", - "libmambapy >=1.4.1", + "conda >=23.7.3", + "libmambapy >=1.5.0", "boltons >=23.0.0", ] dynamic = [ diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 6cb7b8cd..19a90667 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -21,8 +21,8 @@ requirements: - hatch-vcs run: - python >=3.8 - - conda >=23.5.0 - - libmambapy >=1.4.1 + - conda >=23.7.3 + - libmambapy >=1.5.0 - boltons >=23.0.0 test: From c41a9303e5216aebb4ca0ea2da25530c3b090ac9 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 30 Aug 2023 11:17:57 +0200 Subject: [PATCH 25/48] fix tests/core/test_solve.py::test_force_remove_1 and tests/core/test_solve.py::test_pinned_1 --- conda_libmamba_solver/solver.py | 31 ++++++++++++++++++++++--------- conda_libmamba_solver/state.py | 8 +++++++- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 648acfd1..1390075d 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -20,7 +20,7 @@ import libmambapy as api from boltons.setutils import IndexedSet from conda import __version__ as _conda_version -from conda.base.constants import REPODATA_FN, UNKNOWN_CHANNEL, ChannelPriority, on_win +from conda.base.constants import REPODATA_FN, UNKNOWN_CHANNEL, ChannelPriority, on_win, UpdateModifier, DepsModifier from conda.base.context import context from conda.common.constants import NULL from conda.common.io import Spinner @@ -320,8 +320,6 @@ def _setup_solver(self, index: LibMambaIndexHelper): self._solver_options = solver_options = [ (api.SOLVER_FLAG_ALLOW_DOWNGRADE, 1), (api.SOLVER_FLAG_INSTALL_ALSO_UPDATES, 1), - # (api.SOLVER_FLAG_FOCUS_BEST, 1), - # (api.SOLVER_FLAG_BEST_OBEY_POLICY, 1), ] if context.channel_priority is ChannelPriority.STRICT: solver_options.append((api.SOLVER_FLAG_STRICT_REPO_PRIORITY, 1)) @@ -470,17 +468,31 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu tasks[("USERINSTALLED", api.SOLVER_USERINSTALLED)].append(installed_spec_str) # These specs are explicit in some sort of way + if pinned: + # these are the EXPLICIT pins; conda also uses implicit pinning to + # constrain updates too but those can be overridden in case of conflicts. + if requested and not requested.match(pinned): + # We don't pin; requested and pinned are different, requested wins + # we let that happen in the next block + pass + else: + tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(pinned)) + if requested: - tasks[("INSTALL", api.SOLVER_INSTALL)].append(self._spec_to_str(requested)) + spec_str = self._spec_to_str(requested) + if installed: + tasks[("UPDATE", api.SOLVER_UPDATE)].append(spec_str) + tasks[("ALLOW_UNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(name) + else: + tasks[("INSTALL", api.SOLVER_INSTALL)].append(spec_str) elif name in in_state.always_update: tasks[("UPDATE", api.SOLVER_UPDATE)].append(name) - elif pinned: - tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(pinned)) + tasks[("ALLOW_UNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(name) # These specs are "implicit"; the solver logic massages them for better UX # as long as they don't cause trouble elif in_state.prune: continue - elif name == "python" and installed: + elif name == "python" and installed and not pinned: pyver = ".".join(installed.version.split(".")[:2]) tasks[("ADD_PIN", api.SOLVER_NOOP)].append(f"python {pyver}.*") elif history and not history.is_name_only_spec and not conflicting: @@ -490,14 +502,15 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu tasks[("ALLOW_UNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(name) else: # we freeze everything else as installed - lock = True + lock = not in_state.update_modifier.UPDATE_ALL if python_version_might_change and installed.noarch is None: for dep in installed.depends: if MatchSpec(dep).name in ("python", "python_abi"): lock = False break if lock: - tasks[("LOCK", api.SOLVER_LOCK)].append(installed_spec_str) + tasks[("LOCK", api.SOLVER_LOCK | api.SOLVER_WEAK)].append(installed_spec_str) + tasks[("VERIFY", api.SOLVER_VERIFY | api.SOLVER_WEAK)].append(name) return dict(tasks) diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index e2873943..ec2911a6 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -551,7 +551,7 @@ def _initialize_specs_from_input_state(self): if self.solver_input_state.prune: pass # we do not initialize specs with history OR installed pkgs if we are pruning # Otherwise, initialization depends on whether we have a history to work with or not - elif self.solver_input_state.history: + elif self.solver_input_state.history and not self.solver_input_state.update_modifier.UPDATE_ALL: # add in historically-requested specs self.specs.update(self.solver_input_state.history, reason="As in history") for name, record in self.solver_input_state.installed.items(): @@ -794,6 +794,12 @@ def _prepare_for_add(self, index: IndexHelper): reason="Update all, with history: treat pip installed " "stuff as explicitly installed", ) + elif name not in self.specs: + self.specs.set( + name, + MatchSpec(name), + reason="Update all, with history: adding name-only spec from installed", + ) else: for name in sis.installed: if name in sis.pinned: From 07dc6d15995338e464de68d54ed5ed087a83f512 Mon Sep 17 00:00:00 2001 From: Chris Ostrouchov Date: Thu, 7 Sep 2023 23:33:19 -0400 Subject: [PATCH 26/48] Re-enable tests that are failing --- .../collect_upstream_conda_tests.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py index b0b63332..0e6cc81a 100644 --- a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py +++ b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py @@ -30,7 +30,6 @@ # Message expected, but libmamba does not report constraints "test_update_prune_5", # TODO: These ones need further investigation - "test_channel_priority_churn_minimized", "test_priority_1", # The following are known to fail upstream due to too strict expectations # We provide the same tests with adjusted checks in tests/test_modified_upstream.py @@ -46,13 +45,9 @@ ], "tests/test_create.py": [ "test_remove_features", - # Inconsistency analysis not implemented yet - "test_conda_recovery_of_pip_inconsistent_env", # Known bug in mamba; see https://github.com/mamba-org/mamba/issues/1197 "test_offline_with_empty_index_cache", "test_pinned_override_with_explicit_spec", - # TODO: Investigate why this fails on Windows now - "test_install_update_deps_only_deps_flags", # TODO: https://github.com/conda/conda-libmamba-solver/issues/141 "test_conda_pip_interop_conda_editable_package", ], @@ -95,18 +90,6 @@ } _broken_by_libmamba_1_4_2 = { - # conda/tests - "tests/core/test_solve.py": [ - "test_force_remove_1", - "test_aggressive_update_packages", - "test_update_deps_2", - ], - "tests/test_create.py": [ - "test_list_with_pip_wheel", - "test_conda_pip_interop_dependency_satisfied_by_pip", # Linux-only - "test_conda_pip_interop_pip_clobbers_conda", # Linux-only - "test_install_tarball_from_local_channel", # Linux-only - ], # conda-libmamba-solver/tests "tests/test_modified_upstream.py": [ "test_pinned_1", From a39d1d1da93de8cf58a4b0339c9ef2feb41196a8 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 12 Sep 2023 10:15:25 +0200 Subject: [PATCH 27/48] Require libmambapy >= 1.5.1 --- dev/requirements.txt | 4 ++-- pyproject.toml | 2 +- recipe/meta.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/requirements.txt b/dev/requirements.txt index dd616efb..95b46620 100644 --- a/dev/requirements.txt +++ b/dev/requirements.txt @@ -3,7 +3,7 @@ pip # run-time boltons>=23.0.0 conda>=23.7.3 -libmamba>=1.5.0 -libmambapy>=1.5.0 +libmamba>=1.5.1 +libmambapy>=1.5.1 # be explicit about sqlite because sometimes it's removed from the env :shrug: sqlite diff --git a/pyproject.toml b/pyproject.toml index 5753e291..43fc9278 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.8" dependencies = [ "conda >=23.7.3", - "libmambapy >=1.5.0", + "libmambapy >=1.5.1", "boltons >=23.0.0", ] dynamic = [ diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 19a90667..6665e98a 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -22,7 +22,7 @@ requirements: run: - python >=3.8 - conda >=23.7.3 - - libmambapy >=1.5.0 + - libmambapy >=1.5.1 - boltons >=23.0.0 test: From 2c7fa21e5023cb89920d4900add29ad97eaf21fb Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 12 Sep 2023 10:18:14 +0200 Subject: [PATCH 28/48] pre-commit --- conda_libmamba_solver/solver.py | 18 ++++++++++-------- conda_libmamba_solver/state.py | 8 ++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 1390075d..41d19ce8 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -20,7 +20,7 @@ import libmambapy as api from boltons.setutils import IndexedSet from conda import __version__ as _conda_version -from conda.base.constants import REPODATA_FN, UNKNOWN_CHANNEL, ChannelPriority, on_win, UpdateModifier, DepsModifier +from conda.base.constants import REPODATA_FN, UNKNOWN_CHANNEL, ChannelPriority, on_win from conda.base.context import context from conda.common.constants import NULL from conda.common.io import Spinner @@ -427,13 +427,13 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu # installed to fulfill some dependency. This is needed input for the calculation of # unneeded packages for jobs that have the SOLVER_CLEANDEPS flag set." user_installed = { - pkg + pkg for pkg in ( *in_state.history, *in_state.aggressive_updates, *in_state.pinned, - *in_state.do_not_remove - ) + *in_state.do_not_remove, + ) if pkg in in_state.installed } @@ -468,8 +468,8 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu tasks[("USERINSTALLED", api.SOLVER_USERINSTALLED)].append(installed_spec_str) # These specs are explicit in some sort of way - if pinned: - # these are the EXPLICIT pins; conda also uses implicit pinning to + if pinned: + # these are the EXPLICIT pins; conda also uses implicit pinning to # constrain updates too but those can be overridden in case of conflicts. if requested and not requested.match(pinned): # We don't pin; requested and pinned are different, requested wins @@ -497,7 +497,7 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu tasks[("ADD_PIN", api.SOLVER_NOOP)].append(f"python {pyver}.*") elif history and not history.is_name_only_spec and not conflicting: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(history)) - elif installed: + elif installed: if conflicting and not history: tasks[("ALLOW_UNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(name) else: @@ -509,7 +509,9 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu lock = False break if lock: - tasks[("LOCK", api.SOLVER_LOCK | api.SOLVER_WEAK)].append(installed_spec_str) + tasks[("LOCK", api.SOLVER_LOCK | api.SOLVER_WEAK)].append( + installed_spec_str + ) tasks[("VERIFY", api.SOLVER_VERIFY | api.SOLVER_WEAK)].append(name) return dict(tasks) diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index ec2911a6..50bfd9d4 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -551,7 +551,10 @@ def _initialize_specs_from_input_state(self): if self.solver_input_state.prune: pass # we do not initialize specs with history OR installed pkgs if we are pruning # Otherwise, initialization depends on whether we have a history to work with or not - elif self.solver_input_state.history and not self.solver_input_state.update_modifier.UPDATE_ALL: + elif ( + self.solver_input_state.history + and not self.solver_input_state.update_modifier.UPDATE_ALL + ): # add in historically-requested specs self.specs.update(self.solver_input_state.history, reason="As in history") for name, record in self.solver_input_state.installed.items(): @@ -798,7 +801,8 @@ def _prepare_for_add(self, index: IndexHelper): self.specs.set( name, MatchSpec(name), - reason="Update all, with history: adding name-only spec from installed", + reason="Update all, with history: " + "adding name-only spec from installed", ) else: for name in sis.installed: From 03d0e560b61df5df752a102787045978a5289d35 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 12 Sep 2023 10:22:06 +0200 Subject: [PATCH 29/48] amend news --- news/{267-prune => 270-tasks-and-prune} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename news/{267-prune => 270-tasks-and-prune} (57%) diff --git a/news/267-prune b/news/270-tasks-and-prune similarity index 57% rename from news/267-prune rename to news/270-tasks-and-prune index 2e0453ee..9c843f75 100644 --- a/news/267-prune +++ b/news/270-tasks-and-prune @@ -1,12 +1,13 @@ ### Enhancements -* +* Rewrite how we create tasks for `libsolv`, making use of `libmamba`'s `add_pin` features. (#270) ### Bug fixes * Port logic from [conda/conda#9614](https://github.com/conda/conda/pull/9614), which fixes a bug where the `--prune` flag was not working correctly in `conda env update` commands. - (#267) + (#270) +* Ensure environments are not aggressively updated to higher priority channels under some conditions. (#240 via #270) ### Deprecations From 32c443520bf0ccf23dab71e7cf5ec488553ec506 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 12 Sep 2023 16:38:10 +0200 Subject: [PATCH 30/48] Update __init__.py --- conda_libmamba_solver/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conda_libmamba_solver/__init__.py b/conda_libmamba_solver/__init__.py index 6f6a4944..b7b5b31a 100644 --- a/conda_libmamba_solver/__init__.py +++ b/conda_libmamba_solver/__init__.py @@ -29,3 +29,6 @@ def get_solver_class(key="libmamba"): ) return LibMambaSolver raise ValueError("Key must be 'libmamba'") + + +# TRIGGER CI -- REMOVE From 4c2bdc370764e84074aee0b94221418aef79f9de Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 12 Sep 2023 17:16:11 +0200 Subject: [PATCH 31/48] fix test_neutering_of_historic_specs --- conda_libmamba_solver/solver.py | 2 +- conda_libmamba_solver/state.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 41d19ce8..6107f719 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -498,7 +498,7 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu elif history and not history.is_name_only_spec and not conflicting: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(history)) elif installed: - if conflicting and not history: + if conflicting: tasks[("ALLOW_UNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(name) else: # we freeze everything else as installed diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index 50bfd9d4..c7e1a0db 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -1040,7 +1040,9 @@ def post_solve(self, solver: Type["Solver"]): history_spec = sis.history.get(name) if history_spec and spec.strictness < history_spec.strictness: self.neutered.set( - name, spec, reason="Spec needs less strict constrains than history" + name, + MatchSpec(name, version=history_spec.get("version")), + reason="Spec needs less strict constrains than history", ) # ## Add inconsistent packages back ### From 307c0a0b56395fd879fc08dda7c58ef5da32ac8c Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 12 Sep 2023 17:30:07 +0200 Subject: [PATCH 32/48] consider 1.5.1 in warnings too --- conda_libmamba_solver/solver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 6107f719..cde1d6d5 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -369,16 +369,16 @@ def _solve_attempt( try: self.solver.add_jobs(specs, task_type) except RuntimeError as exc: - if mamba_version() == "1.5.0": + if mamba_version() in ("1.5.0", "1.5.1"): for spec in specs: if spec in str(exc): break else: spec = f"One of {specs}" msg = ( - "This is a bug in libmamba 1.5.0 when using 'defaults::" - "' or 'pkgs/main::'. Please use '-c " - "defaults' instead." + f"This is a bug in libmamba {mamba_version()} when using " + "'defaults::' or 'pkgs/main::'. " + "Consider using '-c defaults' instead." ) raise InvalidMatchSpec(spec, msg) raise From 466182d091f8021ecafb2bd1a78a3e511e97dd68 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 12 Sep 2023 17:32:57 +0200 Subject: [PATCH 33/48] pre-commit --- conda_libmamba_solver/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index c7e1a0db..e5135b07 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -1041,7 +1041,7 @@ def post_solve(self, solver: Type["Solver"]): if history_spec and spec.strictness < history_spec.strictness: self.neutered.set( name, - MatchSpec(name, version=history_spec.get("version")), + MatchSpec(name, version=history_spec.get("version")), reason="Spec needs less strict constrains than history", ) From 5af3debb861adc14b40b76a9cfc670971def3b3b Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 12 Sep 2023 17:41:11 +0200 Subject: [PATCH 34/48] adjust test --- tests/test_solvers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 642796e6..81d0d9e9 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -234,5 +234,5 @@ def test_too_aggressive_update_to_conda_forge_packages(): p_libmamba = conda_subprocess(*cmd, "--solver=libmamba", explain=True, env=env) data_classic = json.loads(p_classic.stdout) data_libmamba = json.loads(p_libmamba.stdout) - assert len(data_classic["actions"]["LINK"]) < 10 - assert len(data_libmamba["actions"]["LINK"]) < 10 + assert len(data_classic["actions"]["LINK"]) < 15 + assert len(data_libmamba["actions"]["LINK"]) <= len(data_classic["actions"]["LINK"]) From 7ee5f9f3ef583458c7e1b3e5c8d8e2378f219ac1 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 12 Sep 2023 17:43:51 +0200 Subject: [PATCH 35/48] remove comment --- conda_libmamba_solver/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/conda_libmamba_solver/__init__.py b/conda_libmamba_solver/__init__.py index b7b5b31a..6f6a4944 100644 --- a/conda_libmamba_solver/__init__.py +++ b/conda_libmamba_solver/__init__.py @@ -29,6 +29,3 @@ def get_solver_class(key="libmamba"): ) return LibMambaSolver raise ValueError("Key must be 'libmamba'") - - -# TRIGGER CI -- REMOVE From 86375e26425efc7f75d7be377dfce70f7810e5d4 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 11:14:08 +0200 Subject: [PATCH 36/48] fix json serialization --- conda_libmamba_solver/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index cde1d6d5..077d20d4 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -666,7 +666,7 @@ def _maybe_raise_for_problems( not_found = problems["not_found"] if not unsatisfiable and not_found: # This is not a conflict, but a missing package in the channel - exc = PackagesNotFoundError(not_found.values(), self.channels) + exc = PackagesNotFoundError(tuple(not_found.values()), tuple(self.channels)) exc.allow_retry = False raise exc From 2fab4499f7f92ba9ec457f8d6911b8c4719888c8 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 11:14:16 +0200 Subject: [PATCH 37/48] xfail in 1.5.1 too --- tests/test_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 81d0d9e9..a675cc0f 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -92,7 +92,7 @@ def test_python_downgrade_reinstalls_noarch_packages(): @pytest.mark.xfail( - mamba_version() == "1.5.0", + mamba_version() in ("1.5.0", "1.5.1"), reason="Known bug. See https://github.com/mamba-org/mamba/issues/2431", ) def test_defaults_specs_work(): From 2cccbc67a972f33380fb0894c7c4524d2471e4cb Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 15:48:18 +0200 Subject: [PATCH 38/48] catch RuntimeError for the whole 1.5.x series --- conda_libmamba_solver/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 077d20d4..5fc089b7 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -369,7 +369,7 @@ def _solve_attempt( try: self.solver.add_jobs(specs, task_type) except RuntimeError as exc: - if mamba_version() in ("1.5.0", "1.5.1"): + if mamba_version().startswith("1.5."): for spec in specs: if spec in str(exc): break From 24edbafb866130ac3ab4aaded84d7f78fad73b72 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 15:48:40 +0200 Subject: [PATCH 39/48] xfail test_explicit --- .../collect_upstream_conda_tests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py index 0e6cc81a..8764edce 100644 --- a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py +++ b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py @@ -97,6 +97,14 @@ } +_broken_by_libmamba_1_5_x = { + # conda/tests + "tests/test_export.py": [ + "test_explicit", + ], +} + + def pytest_collection_modifyitems(session, config, items): """ We use this hook to modify which upstream tests (from the conda/conda repo) @@ -116,6 +124,8 @@ def pytest_collection_modifyitems(session, config, items): "libmambapy" ) >= "1.4.2" and item_name_no_brackets in _broken_by_libmamba_1_4_2.get(path_key, []): item.add_marker(pytest.mark.xfail(reason="Broken by libmamba 1.4.2; see #186")) + if version("libmambapy") in ("1.5.0", "1.5.1") and item_name_no_brackets in _broken_by_libmamba_1_5_x.get(path_key, []): + item.add_marker(pytest.mark.xfail(reason="Broken in libmamba 1.5.x; see https://github.com/mamba-org/mamba/issues/2431")) selected.append(item) items[:] = selected config.hook.pytest_deselected(items=deselected) From 225f5c0dcf5169624c1e00e75aee91a5f37a9ae7 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 16:21:29 +0200 Subject: [PATCH 40/48] fix test_pinned_1 --- conda_libmamba_solver/solver.py | 4 ++-- .../collect_upstream_conda_tests.py | 21 +++++++------------ tests/test_modified_upstream.py | 5 ++++- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 5fc089b7..c70b2a8c 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -472,8 +472,8 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu # these are the EXPLICIT pins; conda also uses implicit pinning to # constrain updates too but those can be overridden in case of conflicts. if requested and not requested.match(pinned): - # We don't pin; requested and pinned are different, requested wins - # we let that happen in the next block + # We don't pin; requested and pinned are different and incompatible, + # requested wins and we let that happen in the next block pass else: tasks[("ADD_PIN", api.SOLVER_NOOP)].append(self._spec_to_str(pinned)) diff --git a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py index 8764edce..eb16655e 100644 --- a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py +++ b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py @@ -89,13 +89,6 @@ "tests/test_priority.py": ["test_reorder_channel_priority"], } -_broken_by_libmamba_1_4_2 = { - # conda-libmamba-solver/tests - "tests/test_modified_upstream.py": [ - "test_pinned_1", - ], -} - _broken_by_libmamba_1_5_x = { # conda/tests @@ -120,12 +113,14 @@ def pytest_collection_modifyitems(session, config, items): if item_name_no_brackets in _deselected_upstream_tests.get(path_key, []): deselected.append(item) continue - if version( - "libmambapy" - ) >= "1.4.2" and item_name_no_brackets in _broken_by_libmamba_1_4_2.get(path_key, []): - item.add_marker(pytest.mark.xfail(reason="Broken by libmamba 1.4.2; see #186")) - if version("libmambapy") in ("1.5.0", "1.5.1") and item_name_no_brackets in _broken_by_libmamba_1_5_x.get(path_key, []): - item.add_marker(pytest.mark.xfail(reason="Broken in libmamba 1.5.x; see https://github.com/mamba-org/mamba/issues/2431")) + if version("libmambapy").startswith( + "1.5." + ) and item_name_no_brackets in _broken_by_libmamba_1_5_x.get(path_key, []): + item.add_marker( + pytest.mark.xfail( + reason="Broken in libmamba 1.5.x; see https://github.com/mamba-org/mamba/issues/2431q" + ) + ) selected.append(item) items[:] = selected config.hook.pytest_deselected(items=deselected) diff --git a/tests/test_modified_upstream.py b/tests/test_modified_upstream.py index 3d7824bb..ebcdafee 100644 --- a/tests/test_modified_upstream.py +++ b/tests/test_modified_upstream.py @@ -301,7 +301,10 @@ def test_pinned_1(tmpdir): assert convert_to_dist_str(final_state_5) == order # now update without pinning - specs_to_add = (MatchSpec("python"),) + # MODIFIED: libmamba decides to stay in python=2.6 unless explicit + # specs_to_add = (MatchSpec("python"),) + specs_to_add = (MatchSpec("python=3"),) + # /MODIFIED history_specs = ( MatchSpec("python"), MatchSpec("system=5.8=0"), From 40d59179954cfc8a7171c4b4a87036c352c5703f Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 16:54:18 +0200 Subject: [PATCH 41/48] lock installed name-only pinned packages --- conda_libmamba_solver/solver.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index c70b2a8c..abe9effc 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -471,7 +471,10 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu if pinned: # these are the EXPLICIT pins; conda also uses implicit pinning to # constrain updates too but those can be overridden in case of conflicts. - if requested and not requested.match(pinned): + if pinned.is_name_only_spec: + # pins need to constrain in some way, otherwide is undefined behaviour + pass + elif requested and not requested.match(pinned): # We don't pin; requested and pinned are different and incompatible, # requested wins and we let that happen in the next block pass @@ -502,7 +505,11 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu tasks[("ALLOW_UNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(name) else: # we freeze everything else as installed - lock = not in_state.update_modifier.UPDATE_ALL + if pinned and pinned.is_name_only_spec: + # name-only pins are treated as locks when installed + lock = True + elif in_state.update_modifier.UPDATE_ALL: + lock = False if python_version_might_change and installed.noarch is None: for dep in installed.depends: if MatchSpec(dep).name in ("python", "python_abi"): From 253374854889ded3c0bfdecce6178a0f99e86470 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 16:55:11 +0200 Subject: [PATCH 42/48] do not add installed channels to list if --override-channels in use --- conda_libmamba_solver/solver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index abe9effc..624c4033 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -171,7 +171,10 @@ def solve_final_state( else: IndexHelper = LibMambaIndexHelper - if os.getenv("CONDA_LIBMAMBA_SOLVER_NO_CHANNELS_FROM_INSTALLED"): + if ( + os.getenv("CONDA_LIBMAMBA_SOLVER_NO_CHANNELS_FROM_INSTALLED") + or (getattr(context, "_argparse_args", None) or {}).get("override_channels") + ): # see https://github.com/conda/conda-libmamba-solver/issues/108 channels_from_installed = () else: From efffa955b8b0167f629050d6d397cce385de081e Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 17:16:46 +0200 Subject: [PATCH 43/48] add adjusted variant for test_priority --- .../collect_upstream_conda_tests.py | 3 +- tests/test_modified_upstream.py | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py index eb16655e..da026e36 100644 --- a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py +++ b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py @@ -84,8 +84,7 @@ "tests/conda_env/specs/test_requirements.py": [ "TestRequirements::test_environment", ], - # TODO: Known to fail; should be fixed by - # https://github.com/conda/conda-libmamba-solver/pull/242 + # Added to test_modified_upstream.py "tests/test_priority.py": ["test_reorder_channel_priority"], } diff --git a/tests/test_modified_upstream.py b/tests/test_modified_upstream.py index ebcdafee..28593fb4 100644 --- a/tests/test_modified_upstream.py +++ b/tests/test_modified_upstream.py @@ -33,6 +33,7 @@ from conda.gateways.subprocess import subprocess_call_with_clean_env from conda.models.match_spec import MatchSpec from conda.models.version import VersionOrder +from conda.testing import conda_cli, path_factory, tmp_env from conda.testing.cases import BaseTestCase from conda.testing.helpers import ( add_subdir, @@ -53,6 +54,10 @@ run_command, ) +from pytest import MonkeyPatch + +from conda.testing import CondaCLIFixture, TmpEnvFixture + @pytest.mark.integration class PatchedCondaTestCreate(BaseTestCase): @@ -1255,3 +1260,53 @@ def test_downgrade_python_prevented_with_sane_message(tmpdir): assert "Encountered problems while solving" in error_msg assert "package unsatisfiable-with-py26-1.0-0 requires scikit-learn 0.13" in error_msg ## /MODIFIED + + +# The following tests come from tests/test_priority.py + +@pytest.mark.integration +@pytest.mark.parametrize( + "pinned_package", + [ + pytest.param(True, id="with pinned_package"), + pytest.param(False, id="without pinned_package"), + ], +) +def test_reorder_channel_priority( + tmp_env: TmpEnvFixture, + monkeypatch: MonkeyPatch, + conda_cli: CondaCLIFixture, + pinned_package: bool, +): + # use "cheap" packages with no dependencies + package1 = "zlib" + package2 = "ca-certificates" + + # set pinned package + if pinned_package: + monkeypatch.setenv("CONDA_PINNED_PACKAGES", package1) + + # create environment with package1 and package2 + with tmp_env( + "--override-channels", "--channel=defaults", package1, package2 + ) as prefix: + # check both packages are installed from defaults + PrefixData._cache_.clear() + assert PrefixData(prefix).get(package1).channel.name == "pkgs/main" + assert PrefixData(prefix).get(package2).channel.name == "pkgs/main" + + # update --all + out, err, retcode = conda_cli( + "update", + f"--prefix={prefix}", + "--override-channels", + "--channel=conda-forge", + "--all", + "--yes", + ) + # check pinned package is unchanged but unpinned packages are updated from conda-forge + PrefixData._cache_.clear() + expected_channel = "pkgs/main" if pinned_package else "conda-forge" + assert PrefixData(prefix).get(package1).channel.name == expected_channel + # assert PrefixData(prefix).get(package2).channel.name == "conda-forge" + # MODIFIED ^: Some packages do not change channels in libmamba From 67417ce1b3cb781ee8820c468b994645f8e9501f Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 17:48:35 +0200 Subject: [PATCH 44/48] always define lock --- conda_libmamba_solver/solver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 624c4033..0ebe06da 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -508,6 +508,7 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu tasks[("ALLOW_UNINSTALL", api.SOLVER_ALLOWUNINSTALL)].append(name) else: # we freeze everything else as installed + lock = True if pinned and pinned.is_name_only_spec: # name-only pins are treated as locks when installed lock = True From 47843a19f146546b8e5b819d55de329ba40b8fc3 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 17:48:58 +0200 Subject: [PATCH 45/48] explain some deselected tests --- .../collect_upstream_conda_tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py index da026e36..0faf717a 100644 --- a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py +++ b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py @@ -29,7 +29,8 @@ "test_update_prune_3", # Message expected, but libmamba does not report constraints "test_update_prune_5", - # TODO: These ones need further investigation + # classic expects implicit update to channel with higher priority, including downgrades + # libmamba does not do this, it just stays in the same channel; should it change? "test_priority_1", # The following are known to fail upstream due to too strict expectations # We provide the same tests with adjusted checks in tests/test_modified_upstream.py @@ -44,9 +45,11 @@ "test_downgrade_python_prevented_with_sane_message", ], "tests/test_create.py": [ + # libmamba does not support features "test_remove_features", # Known bug in mamba; see https://github.com/mamba-org/mamba/issues/1197 "test_offline_with_empty_index_cache", + # Adjusted in tests/test_modified_upstream.py "test_pinned_override_with_explicit_spec", # TODO: https://github.com/conda/conda-libmamba-solver/issues/141 "test_conda_pip_interop_conda_editable_package", From 9e17e10e3ec52107dcc610db7c6a059ff1fbd482 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 18:15:36 +0200 Subject: [PATCH 46/48] adjust test_install_features --- .../collect_upstream_conda_tests.py | 1 + tests/test_modified_upstream.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py index 0faf717a..88c4457c 100644 --- a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py +++ b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py @@ -50,6 +50,7 @@ # Known bug in mamba; see https://github.com/mamba-org/mamba/issues/1197 "test_offline_with_empty_index_cache", # Adjusted in tests/test_modified_upstream.py + "test_install_features", "test_pinned_override_with_explicit_spec", # TODO: https://github.com/conda/conda-libmamba-solver/issues/141 "test_conda_pip_interop_conda_editable_package", diff --git a/tests/test_modified_upstream.py b/tests/test_modified_upstream.py index 28593fb4..9b91b4de 100644 --- a/tests/test_modified_upstream.py +++ b/tests/test_modified_upstream.py @@ -107,6 +107,32 @@ def test_install_update_deps_only_deps_flags(self): assert package_is_installed(prefix, "jinja2>3.0.1") +@pytest.mark.xfail(on_win, reason="nomkl not present on windows", strict=True) +def test_install_features(): + # MODIFIED: Added fixture manually + PackageCacheData.clear() + # /MODIFIED + with make_temp_env("python=2", "numpy=1.13", "nomkl", no_capture=True) as prefix: + assert package_is_installed(prefix, "numpy") + assert package_is_installed(prefix, "nomkl") + assert not package_is_installed(prefix, "mkl") + + with make_temp_env("python=2", "numpy=1.13") as prefix: + assert package_is_installed(prefix, "numpy") + assert not package_is_installed(prefix, "nomkl") + assert package_is_installed(prefix, "mkl") + + # run_command(Commands.INSTALL, prefix, "nomkl", no_capture=True) + run_command(Commands.INSTALL, prefix, "python=2", "nomkl", no_capture=True) + # MODIFIED ^: python=2 needed explicitly to trigger update + assert package_is_installed(prefix, "numpy") + assert package_is_installed(prefix, "nomkl") + assert package_is_installed(prefix, "blas=1.0=openblas") + assert not package_is_installed(prefix, "mkl_fft") + assert not package_is_installed(prefix, "mkl_random") + # assert not package_is_installed(prefix, "mkl") # pruned as an indirect dep + + # The following tests come from `conda/conda::tests/core/test_solve.py` From 8e300d8d77a914c438abcf3977fd811a6f24faf5 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 18:17:44 +0200 Subject: [PATCH 47/48] xfail test_export --- .../collect_upstream_conda_tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py index 88c4457c..78670a40 100644 --- a/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py +++ b/dev/collect_upstream_conda_tests/collect_upstream_conda_tests.py @@ -97,6 +97,7 @@ # conda/tests "tests/test_export.py": [ "test_explicit", + "test_export", ], } @@ -121,7 +122,8 @@ def pytest_collection_modifyitems(session, config, items): ) and item_name_no_brackets in _broken_by_libmamba_1_5_x.get(path_key, []): item.add_marker( pytest.mark.xfail( - reason="Broken in libmamba 1.5.x; see https://github.com/mamba-org/mamba/issues/2431q" + reason="Broken in libmamba 1.5.x; " + "see https://github.com/mamba-org/mamba/issues/2431." ) ) selected.append(item) From 1aeaf0a274babbfebbb1ed1408d210540ff334d8 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 13 Sep 2023 18:18:16 +0200 Subject: [PATCH 48/48] pre-commit --- conda_libmamba_solver/solver.py | 7 +++---- tests/test_modified_upstream.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 0ebe06da..fc5efe6f 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -171,10 +171,9 @@ def solve_final_state( else: IndexHelper = LibMambaIndexHelper - if ( - os.getenv("CONDA_LIBMAMBA_SOLVER_NO_CHANNELS_FROM_INSTALLED") - or (getattr(context, "_argparse_args", None) or {}).get("override_channels") - ): + if os.getenv("CONDA_LIBMAMBA_SOLVER_NO_CHANNELS_FROM_INSTALLED") or ( + getattr(context, "_argparse_args", None) or {} + ).get("override_channels"): # see https://github.com/conda/conda-libmamba-solver/issues/108 channels_from_installed = () else: diff --git a/tests/test_modified_upstream.py b/tests/test_modified_upstream.py index 9b91b4de..417db390 100644 --- a/tests/test_modified_upstream.py +++ b/tests/test_modified_upstream.py @@ -33,7 +33,13 @@ from conda.gateways.subprocess import subprocess_call_with_clean_env from conda.models.match_spec import MatchSpec from conda.models.version import VersionOrder -from conda.testing import conda_cli, path_factory, tmp_env +from conda.testing import ( + CondaCLIFixture, + TmpEnvFixture, + conda_cli, + path_factory, + tmp_env, +) from conda.testing.cases import BaseTestCase from conda.testing.helpers import ( add_subdir, @@ -53,11 +59,8 @@ package_is_installed, run_command, ) - from pytest import MonkeyPatch -from conda.testing import CondaCLIFixture, TmpEnvFixture - @pytest.mark.integration class PatchedCondaTestCreate(BaseTestCase): @@ -1290,6 +1293,7 @@ def test_downgrade_python_prevented_with_sane_message(tmpdir): # The following tests come from tests/test_priority.py + @pytest.mark.integration @pytest.mark.parametrize( "pinned_package", @@ -1313,9 +1317,7 @@ def test_reorder_channel_priority( monkeypatch.setenv("CONDA_PINNED_PACKAGES", package1) # create environment with package1 and package2 - with tmp_env( - "--override-channels", "--channel=defaults", package1, package2 - ) as prefix: + with tmp_env("--override-channels", "--channel=defaults", package1, package2) as prefix: # check both packages are installed from defaults PrefixData._cache_.clear() assert PrefixData(prefix).get(package1).channel.name == "pkgs/main"