diff --git a/mamba_gator/envmanager.py b/mamba_gator/envmanager.py index dfd80d1d..c114932f 100644 --- a/mamba_gator/envmanager.py +++ b/mamba_gator/envmanager.py @@ -698,17 +698,19 @@ async def pkg_depends(self, pkg: str) -> Dict[str, List[str]]: Returns: {"package": List[dependencies]} """ - if not self.is_mamba(): + resp = {} + + ans = await self._execute(self.manager, "repoquery", "depends", "--json", pkg) + rcode, output = ans + + if rcode != 0: self.log.warning( - "Package manager '{}' does not support dependency query.".format( + "Package manager '{}' does not support dependency query. " + "Ensure conda-libmamba-solver is installed or use mamba.".format( self.manager ) ) return {pkg: None} - - resp = {} - ans = await self._execute(self.manager, "repoquery", "depends", "--json", pkg) - _, output = ans query = self._clean_conda_json(output) if "error" not in query: @@ -732,11 +734,15 @@ async def list_available(self) -> Dict[str, List[Dict[str, str]]]: "with_description": bool # Whether we succeed in get some channeldata.json files } """ - if self.is_mamba(): - ans = await self._execute(self.manager, "repoquery", "search", "*", "--json") - else: + ans = await self._execute(self.manager, "repoquery", "search", "*", "--json") + rcode, output = ans + + if rcode != 0: + # Fall back to conda search for older conda versions + self.log.debug("repoquery not available, falling back to conda search") + print("Fall back to conda search since repoquery not available") ans = await self._execute(self.manager, "search", "--json") - _, output = ans + _, output = ans current_loop = tornado.ioloop.IOLoop.current() data = await current_loop.run_in_executor(None, self._clean_conda_json, output) @@ -760,7 +766,7 @@ def process_mamba_repoquery_output(data: Dict) -> Dict: return data_ - if self.is_mamba(): + if rcode == 0: data = await current_loop.run_in_executor(None, process_mamba_repoquery_output, data) def format_packages(data: Dict) -> List: diff --git a/mamba_gator/tests/test_api.py b/mamba_gator/tests/test_api.py index 8b967172..4415415d 100644 --- a/mamba_gator/tests/test_api.py +++ b/mamba_gator/tests/test_api.py @@ -1122,11 +1122,16 @@ async def test_package_list_available(conda_fetch, wait_for_task): if has_mamba: dummy = {"result": {"pkgs": list(chain(*dummy.values()))}} - - f.side_effect = [ - (0, json.dumps(dummy)), - (0, json.dumps(channels)), - ] + f.side_effect = [ + (0, json.dumps(dummy)), + (0, json.dumps(channels)), + ] + else: + f.side_effect = [ + (1, "repoquery not available") + (0, json.dumps(dummy)), + (0, json.dumps(channels)), + ] response = await conda_fetch("packages", method="GET") assert response.code == 202 @@ -1134,10 +1139,11 @@ async def test_package_list_available(conda_fetch, wait_for_task): response = await wait_for_task(location) assert response.code == 200 - args, _ = f.call_args_list[0] - if has_mamba: + if has_mambe: + args, _ = f.call_args_list[0] assert args[1:] == ("repoquery", "search", "*", "--json") else: + args, _ = f.call_args_list[1] assert args[1:] == ("search", "--json") body = json.loads(response.body) @@ -1308,6 +1314,16 @@ async def test_package_list_available_local_channel(conda_fetch, wait_for_task): if has_mamba: dummy = {"result": {"pkgs": list(chain(*dummy.values()))}} + f.side_effect = [ + (0, json.dumps(dummy)), + (0, json.dumps(channels)), + ] + else: + f.side_effect = [ + (1, "repoquery not available") + (0, json.dumps(dummy)), + (0, json.dumps(channels)), + ] with tempfile.TemporaryDirectory() as local_channel: with open(os.path.join(local_channel, "channeldata.json"), "w+") as d: @@ -1353,10 +1369,11 @@ async def test_package_list_available_local_channel(conda_fetch, wait_for_task): response = await wait_for_task(location) assert response.code == 200 - args, _ = f.call_args_list[0] - if has_mamba: + if has_mambe: + args, _ = f.call_args_list[0] assert args[1:] == ("repoquery", "search", "*", "--json") else: + args, _ = f.call_args_list[1] assert args[1:] == ("search", "--json") body = json.loads(response.body) @@ -1527,6 +1544,16 @@ async def test_package_list_available_no_description(conda_fetch, wait_for_task) if has_mamba: dummy = {"result": {"pkgs": list(chain(*dummy.values()))}} + f.side_effect = [ + (0, json.dumps(dummy)), + (0, json.dumps(channels)), + ] + else: + f.side_effect = [ + (1, "repoquery not available") + (0, json.dumps(dummy)), + (0, json.dumps(channels)), + ] with tempfile.TemporaryDirectory() as local_channel: local_name = local_channel.strip("/") @@ -1558,10 +1585,11 @@ async def test_package_list_available_no_description(conda_fetch, wait_for_task) response = await wait_for_task(location) assert response.code == 200 - args, _ = f.call_args_list[0] - if has_mamba: + if has_mambe: + args, _ = f.call_args_list[0] assert args[1:] == ("repoquery", "search", "*", "--json") else: + args, _ = f.call_args_list[1] assert args[1:] == ("search", "--json") body = json.loads(response.body) @@ -1758,11 +1786,16 @@ async def test_package_list_available_caching(conda_fetch, wait_for_task): if has_mamba: dummy = {"result": {"pkgs": list(chain(*dummy.values()))}} - - f.side_effect = [ - (0, json.dumps(dummy)), - (0, json.dumps(channels)), - ] + f.side_effect = [ + (0, json.dumps(dummy)), + (0, json.dumps(channels)), + ] + else: + f.side_effect = [ + (1, "repoquery not available") + (0, json.dumps(dummy)), + (0, json.dumps(channels)), + ] # First retrieval - no cache available response = await conda_fetch("packages", method="GET") @@ -1771,10 +1804,11 @@ async def test_package_list_available_caching(conda_fetch, wait_for_task): response = await wait_for_task(location) assert response.code == 200 - args, _ = f.call_args_list[0] - if has_mamba: + if has_mambe: + args, _ = f.call_args_list[0] assert args[1:] == ("repoquery", "search", "*", "--json") else: + args, _ = f.call_args_list[1] assert args[1:] == ("search", "--json") expected = { diff --git a/mamba_gator/tests/test_manager.py b/mamba_gator/tests/test_manager.py index c898d13b..ae71ee8d 100644 --- a/mamba_gator/tests/test_manager.py +++ b/mamba_gator/tests/test_manager.py @@ -1,17 +1,61 @@ from pathlib import Path import pytest +import json from packaging.version import Version, InvalidVersion +from unittest.mock import AsyncMock, patch from mamba_gator.envmanager import EnvManager, parse_version -from .utils import has_mamba - def test_envmanager_manager(): + EnvManager._manager_exe = None + manager = EnvManager("", None) + stem = Path(manager.manager).stem + assert stem in ("mamba", "conda"), f"Unexpected manager: {stem}" + assert manager.is_mamba() == (stem == "mamba") + +FAKE_CONDA_SEARCH_OUTPUT = json.dumps({ + "numpy": [ + { + "build": "py39_0", + "build_number": 0, + "channel": "https://repo.anaconda.com/pkgs/main/osx-64", + "name": "numpy", + "platform": "osx-64", + "version": "1.21.0" + }, + { + "build": "py39_1", + "build_number": 1, + "channel": "https://repo.anaconda.com/pkgs/main/osx-64", + "name": "numpy", + "platform": "osx-64", + "version": "1.22.0" + } + ] +}) + +@pytest.mark.asyncio +async def test_list_available_conda_search_fallback(): + """Test that list_available works when repoquery fails and we fall back to conda search.""" manager = EnvManager("", None) - expected = "mamba" if has_mamba else "conda" - assert Path(manager.manager).stem == expected + async def mock_execute(cmd, *args): + if "repoquery" in args: + return (1, "") # repoquery fails + if "search" in args: + return (0, FAKE_CONDA_SEARCH_OUTPUT) + # Pass through for config/channels calls + return await original_execute(cmd, *args) + original_execute = manager._execute + with patch.object(manager, "_execute", side_effect=mock_execute): + result = await manager.list_available() + assert "packages" in result + pkgs = result["packages"] + assert len(pkgs) == 1 + assert pkgs[0]["name"] == "numpy" + assert isinstance(pkgs[0]["version"], list) + assert "1.22.0" in pkgs[0]["version"] def test_parse_r_style_version_with_underscore(): @@ -54,6 +98,7 @@ async def test_list_available_returns_valid_structure(): """Test that list_available returns the expected data structure.""" manager = EnvManager("", None) result = await manager.list_available() + print('@@@@@result', result) assert "packages" in result assert "with_description" in result diff --git a/packages/common/src/components/PkgGraph.tsx b/packages/common/src/components/PkgGraph.tsx index bf161348..8ee9abff 100644 --- a/packages/common/src/components/PkgGraph.tsx +++ b/packages/common/src/components/PkgGraph.tsx @@ -96,16 +96,16 @@ export class PkgGraph extends React.Component { // Manager does not support dependency query error = ( - Please install{' '} + Dependenct query not supported. Ensure{' '} - mamba + conda-libmamba-solver {' '} - manager to resolve dependencies. + is installed or use mamba. ); } else if (