Skip to content

Commit

Permalink
Add a --prune flag to just filter packages within a name
Browse files Browse the repository at this point in the history
  • Loading branch information
jaimergp committed Jul 12, 2024
1 parent f0d6976 commit b2b356b
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 3 deletions.
9 changes: 8 additions & 1 deletion conda_subchannel/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ def configure_parser(parser: argparse.ArgumentParser):
action="append",
help="Keep packages matching this spec only. Can be used several times.",
)
parser.add_argument(
"--prune",
metavar="SPEC",
action="append",
help="Remove the distributions of this package name that do not match this spec.",
)
parser.add_argument(
"--remove",
metavar="SPEC",
Expand All @@ -109,7 +115,7 @@ def configure_parser(parser: argparse.ArgumentParser):


def execute(args: argparse.Namespace) -> int:
if not any([args.after, args.before, args.keep, args.remove, args.keep_tree]):
if not any([args.after, args.before, args.keep, args.remove, args.keep_tree, args.prune]):
raise ArgumentError("Please provide at least one filter.")

with Spinner("Syncing source channel"):
Expand All @@ -124,6 +130,7 @@ def execute(args: argparse.Namespace) -> int:
"subdir_datas": subdir_datas,
"specs_to_keep": args.keep,
"specs_to_remove": args.remove,
"specs_to_prune": args.prune,
"trees_to_keep": args.keep_tree,
"after": args.after,
"before": args.before,
Expand Down
14 changes: 14 additions & 0 deletions conda_subchannel/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@ def _reduce_index(
subdir_datas: Iterable[SubdirData],
specs_to_keep: Iterable[str | MatchSpec] | None = None,
specs_to_remove: Iterable[str | MatchSpec] | None = None,
specs_to_prune: Iterable[str | MatchSpec] | None = None,
trees_to_keep: Iterable[str | MatchSpec] | None = None,
after: int | None = None,
before: int | None = None,
) -> dict[tuple[str, str], PackageRecord]:
specs_to_keep = [MatchSpec(spec) for spec in (specs_to_keep or ())]
specs_to_remove = [MatchSpec(spec) for spec in (specs_to_remove or ())]
specs_to_prune = [MatchSpec(spec) for spec in (specs_to_prune or ())]
trees_to_keep = [MatchSpec(spec) for spec in (trees_to_keep or ())]
if trees_to_keep or specs_to_keep or after is not None or before is not None:
records = {}
Expand Down Expand Up @@ -136,10 +138,22 @@ def _reduce_index(

# Now that we know what to keep, we remove stuff
to_remove = set()

# Of the packages that survived the keeping, we will remove the ones that do not match the
# prune filter
for spec in specs_to_prune:
for key, record in records.items():
if spec.name != record.name:
continue # ignore if the name doesn't match
if not spec.match(record):
to_remove.add(key)

# These are the explicit removals; if you match this, you are out
for spec in specs_to_remove:
for key, record in records.items():
if spec.match(record):
to_remove.add(key)

for key in to_remove:
records.pop(key)

Expand Down
21 changes: 20 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Usage

## CLI

```
$ conda subchannel --help
usage: conda subchannel -c CHANNEL [--repodata-fn REPODATA_FN] [--base-url BASE_URL] [--output PATH] [--subdir PLATFORM] [--after TIME] [--before TIME] [--keep-tree SPEC] [--keep SPEC] [--remove SPEC] [-h]
Expand All @@ -22,6 +24,23 @@ options:
--keep-tree SPEC Keep packages matching this spec and their dependencies. Can be used
several times.
--keep SPEC Keep packages matching this spec only. Can be used several times.
--prune SPEC Remove the distributions of this package name that do not match the
given constraints.
--remove SPEC Remove packages matching this spec. Can be used several times.
-h, --help Show this help message and exit.
```
```


## Filtering algorithm

The filtering algorithm operates in two phases: selection

In the first phase, we _select_ which records are going to be kept. Everything else is removed.

1. A selection list is built. Records in this list are added if:
- They match specs in `--keep-tree`, or any of the dependencies in their tree (assessed recursively).
- They match any of the specs in `--keep`.
- Their timestamp is within the limits marked by `--before` and `--after`, when applicable.
2. At this point, records that didn't make it to the selection list are removed.
3. The specs defined `--prune` are processed. Records that have the same name but don't match the spec are removed. Everything else is ignored.
4. Records matching any of the specs in `--remove` are filtered out.
72 changes: 71 additions & 1 deletion tests/test_subchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def test_only_python(conda_cli, tmp_path):
assert tested


def test_python_tree(conda_cli, tmp_path, monkeypatch):
def test_python_tree(conda_cli, tmp_path):
spec = "python=3.9"
channel_path = tmp_path / "channel"
out, err, rc = conda_cli(
Expand Down Expand Up @@ -104,6 +104,19 @@ def test_python_tree(conda_cli, tmp_path, monkeypatch):
"python=3.10",
)

# This should fail too; nodejs doesn't match python=3.9, so it must be out
with pytest.raises(PackagesNotFoundError):
conda_cli(
"create",
"--dry-run",
"-n",
"unused",
"--override-channels",
"--channel",
channel_path,
"nodejs",
)


def test_not_python(conda_cli, tmp_path):
out, err, rc = conda_cli(
Expand Down Expand Up @@ -253,3 +266,60 @@ def test_served_at(conda_cli, tmp_path):

for path in tmp_path.glob("**/index.html"):
assert served_at in path.read_text()


def test_pruned_python(conda_cli, tmp_path):
spec = "python=3.9"
channel_path = tmp_path / "channel"
out, err, rc = conda_cli(
"subchannel",
"-c",
"conda-forge",
"--prune",
spec,
"--output",
channel_path,
)
print(out)
print(err, file=sys.stderr)
assert rc == 0

# This should be solvable, we didn't remove anything other than non-39 pythons
with pytest.raises(DryRunExit):
conda_cli(
"create",
"--dry-run",
"-n",
"unused",
"--override-channels",
"--channel",
channel_path,
"python=3.9",
)

# This should be unsolvable, we didn't take Python 3.10 in the subchannel
with pytest.raises(PackagesNotFoundError):
conda_cli(
"create",
"--dry-run",
"-n",
"unused",
"--override-channels",
"--channel",
channel_path,
"python=3.10",
)

# This should work because, we just removed pythons that are not python=3.9, but the rest
# of the conda-forge packages should be there
with pytest.raises(DryRunExit):
conda_cli(
"create",
"--dry-run",
"-n",
"unused",
"--override-channels",
"--channel",
channel_path,
"nodejs",
)

0 comments on commit b2b356b

Please sign in to comment.