diff --git a/libmamba/src/api/list.cpp b/libmamba/src/api/list.cpp index a26d6be816..9623f904ca 100644 --- a/libmamba/src/api/list.cpp +++ b/libmamba/src/api/list.cpp @@ -32,6 +32,7 @@ namespace mamba bool canonical = false; bool export_ = false; bool revisions = false; + bool direct_deps_only = false; }; struct formatted_pkg @@ -152,6 +153,7 @@ namespace mamba { return (regex.empty() || std::regex_search(pkg_info.name, spec_pat)); }; auto all_records = prefix_data.all_pkg_mgr_records(); + auto requested_specs = prefix_data.history().get_requested_specs_map(); if (ctx.output_params.json) { @@ -185,6 +187,12 @@ namespace mamba auto obj = nlohmann::json(); const auto& pkg_info = all_records.find(key)->second; + if (options.direct_deps_only + && requested_specs.find(pkg_info.name) == requested_specs.end()) + { + continue; + } + if (accept_package(pkg_info)) { auto channels = channel_context.make_channel(pkg_info.package_url); @@ -213,25 +221,29 @@ namespace mamba << "\n\n"; formatted_pkg formatted_pkgs; - std::vector packages; - // order list of packages from prefix_data by alphabetical order - for (const auto& package : all_records) + for (const auto& [key, pkg_info] : all_records) { - if (accept_package(package.second)) + if (options.direct_deps_only + && requested_specs.find(pkg_info.name) == requested_specs.end()) + { + continue; + } + + if (accept_package(pkg_info)) { - auto channels = channel_context.make_channel(package.second.channel); - assert(channels.size() == 1); // A URL can only resolve to one channel - formatted_pkgs.channel = get_formatted_channel(package.second, channels.front()); - formatted_pkgs.name = package.second.name; - formatted_pkgs.version = package.second.version; - formatted_pkgs.build = package.second.build_string; - formatted_pkgs.url = package.second.package_url; - formatted_pkgs.md5 = package.second.md5; - formatted_pkgs.sha256 = package.second.sha256; - formatted_pkgs.build_string = package.second.build_string; - formatted_pkgs.platform = package.second.platform; + auto channels = channel_context.make_channel(pkg_info.channel); + assert(channels.size() == 1); + formatted_pkgs.channel = get_formatted_channel(pkg_info, channels.front()); + formatted_pkgs.name = pkg_info.name; + formatted_pkgs.version = pkg_info.version; + formatted_pkgs.build = pkg_info.build_string; + formatted_pkgs.url = pkg_info.package_url; + formatted_pkgs.md5 = pkg_info.md5; + formatted_pkgs.sha256 = pkg_info.sha256; + formatted_pkgs.build_string = pkg_info.build_string; + formatted_pkgs.platform = pkg_info.platform; packages.push_back(formatted_pkgs); } } @@ -240,7 +252,6 @@ namespace mamba : compare_alphabetically; std::sort(packages.begin(), packages.end(), comparator); - // format and print output if (options.revisions) { if (options.explicit_) @@ -333,7 +344,6 @@ namespace mamba } else { - auto requested_specs = prefix_data.history().get_requested_specs_map(); printers::Table t({ "Name", "Version", "Build", "Channel" }); t.set_alignment({ printers::alignment::left, printers::alignment::left, @@ -379,6 +389,7 @@ namespace mamba options.canonical = config.at("canonical").value(); options.export_ = config.at("export").value(); options.revisions = config.at("revisions").value(); + options.direct_deps_only = config.at("direct_deps_only").value(); auto channel_context = ChannelContext::make_conda_compatible(config.context()); detail::list_packages(config.context(), regex, channel_context, std::move(options)); diff --git a/micromamba/src/list.cpp b/micromamba/src/list.cpp index 613dd4ff78..109296b34b 100644 --- a/micromamba/src/list.cpp +++ b/micromamba/src/list.cpp @@ -80,6 +80,17 @@ init_list_parser(CLI::App* subcom, Configuration& config) Configurable("revisions", false).group("cli").description("List the revision history.") ); subcom->add_flag("--revisions", revisions.get_cli_config(), revisions.description()); + + auto& direct_deps_only = config.insert( + Configurable("direct_deps_only", false) + .group("cli") + .description("Show only directly installed packages (user requested).") + ); + subcom->add_flag( + "--direct-deps-only", + direct_deps_only.get_cli_config(), + direct_deps_only.description() + ); } void diff --git a/micromamba/tests/test_list.py b/micromamba/tests/test_list.py index 8accb86b61..00065fb15d 100644 --- a/micromamba/tests/test_list.py +++ b/micromamba/tests/test_list.py @@ -1,6 +1,7 @@ import platform import subprocess import sys +import json import pytest import re @@ -154,6 +155,72 @@ def test_list_name(tmp_home, tmp_root_prefix, tmp_xtensor_env, quiet_flag): assert full_names == ["xtensor"] +@pytest.mark.parametrize("direct_deps_flag", ["", "--direct-deps-only"]) +@pytest.mark.parametrize("env_selector", ["", "name", "prefix"]) +@pytest.mark.parametrize("shared_pkgs_dirs", [True], indirect=True) +def test_list_direct_deps_only_json( + tmp_home, tmp_root_prefix, tmp_env_name, tmp_xtensor_env, env_selector, direct_deps_flag +): + """Check filtering with --direct-deps-only and JSON output.""" + if env_selector == "prefix": + res_str = helpers.umamba_list("-p", tmp_xtensor_env, "--json", direct_deps_flag) + elif env_selector == "name": + res_str = helpers.umamba_list("-n", tmp_env_name, "--json", direct_deps_flag) + else: + # Use current env + res_str = helpers.umamba_list("--json", direct_deps_flag) + + res = json.loads(res_str) + names = [i["name"] for i in res] + + if direct_deps_flag == "--direct-deps-only": + assert "xtensor" in names # Explicitly installed + assert "xtl" not in names # Dependency + assert len(names) == 1 # Only xtensor should be listed + else: + assert "xtensor" in names + assert "xtl" in names # Dependency should be included without the flag + assert len(names) > 1 + + assert all( + i["channel"] == "conda-forge" and i["base_url"] == "https://conda.anaconda.org/conda-forge" + for i in res + ) + + +@pytest.mark.parametrize("direct_deps_flag", ["", "--direct-deps-only"]) +@pytest.mark.parametrize("env_selector", ["", "name", "prefix"]) +@pytest.mark.parametrize("shared_pkgs_dirs", [True], indirect=True) +def test_list_direct_deps_only_no_json( + tmp_home, tmp_root_prefix, tmp_env_name, tmp_xtensor_env, env_selector, direct_deps_flag +): + """Check filtering with --direct-deps-only and table output.""" + if env_selector == "prefix": + res = helpers.umamba_list("-p", tmp_xtensor_env, direct_deps_flag) + elif env_selector == "name": + res = helpers.umamba_list("-n", tmp_env_name, direct_deps_flag) + else: + # Use current env + res = helpers.umamba_list(direct_deps_flag) + + # Split lines and ignore header/blank lines + lines = [line.strip() for line in res.strip().split("\n") if line] + package_lines = [ + line + for line in lines + if not line.startswith("#") and "Name" not in line and "Version" not in line + ] + + if direct_deps_flag == "--direct-deps-only": + assert any("xtensor" in line for line in package_lines) + assert not any("xtl" in line for line in package_lines) + assert len(package_lines) == 1 # Only xtensor should be listed + else: + assert any("xtensor" in line for line in package_lines) + assert any("xtl" in line for line in package_lines) + assert len(package_lines) > 1 + + env_yaml_content_to_install_numpy_with_pip = """ channels: - conda-forge