Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a --prune flag to just filter packages within a name #20

Merged
merged 1 commit into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
)