Skip to content

Commit 3db8e1c

Browse files
authored
support pixi environments (#42)
* refactor `spec` into a separate submodule * dispatch by kind * default the kind to `conda` * adapt the tests * configure project metadata * add `manifest_path` to the cli options * formatting * write the environment parsing for pixi envs * raise a more descriptive error if no suitable releases were found * filter out excluded packages before fetching releases * consider unpinned specs as not matching * gracefully handle unpinned but not ignored dependencies * install the module instead of modifying `sys.path` * allow passing `manifest-path` to the action * misformatted input definition * stop testing on the unsupported python 3.10 * install the package itself * use the warning style for unpinned versions * warn about ignored PyPI dependencies * tests for most of the functions in `minimum_versions.release` * tests for the environment functions * tests for parsing conda specs * also check parsing the entire conda env * rename * check parsing pixi specs * fix a bug in the lower pin regex * additional spec parsing checks * check invalid versions raise * checks for parse_pixi_environment * include the default feature in the features * rename the `environment-paths` input to `environments` * describe how to analyze `pixi` environments * rename `env-paths` to `envs` * e2e tests for pixi and mixed envs * typo * add policy files for the pixi tests * another typo * quotes and expected failure settings * add a failing pixi env * also check the default pypi-dependencies * back to `python=3.9` * support the `no-default-feature` option * support analyzing missing features * change the error text for unknown version specs * add a note containing the package name * add more information * support dict pins * configure coverage * check the format detection * raise on unknown features * support no features * check that `<=` is also detected * check pypi dependencies * support local packages * skip the local package, if any * properly check that local packages are skipped * add information about the environments
1 parent 249d38f commit 3db8e1c

File tree

16 files changed

+975
-97
lines changed

16 files changed

+975
-97
lines changed

.github/workflows/ci.yml

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
python-version: ["3.10", "3.11", "3.12", "3.13"]
19+
python-version: ["3.11", "3.12", "3.13"]
2020

2121
steps:
2222
- name: clone the repository
@@ -31,6 +31,7 @@ jobs:
3131
- name: install dependencies
3232
run: |
3333
python -m pip install -r requirements.txt
34+
python -m pip install .
3435
python -m pip install pytest
3536
- name: run tests
3637
run: |
@@ -43,7 +44,7 @@ jobs:
4344
strategy:
4445
fail-fast: false
4546
matrix:
46-
env-paths:
47+
envs:
4748
- "envs/env1.yaml"
4849
- "envs/env2.yaml"
4950
- |
@@ -52,18 +53,38 @@ jobs:
5253
expected-failure: ["false"]
5354
policy-file: ["policy.yaml"]
5455
include:
55-
- env-paths: |
56+
- envs: |
5657
envs/failing-env1.yaml
5758
policy-file: "policy.yaml"
5859
expected-failure: "true"
59-
- env-paths: |
60+
- envs: |
6061
envs/env1.yaml
6162
envs/failing-env1.yaml
6263
policy-file: "policy.yaml"
6364
expected-failure: "true"
64-
- env-paths: "envs/env1.yaml"
65-
policy-file: policy_no_extra_options.yaml
65+
- envs: "envs/env1.yaml"
66+
policy-file: "policy_no_extra_options.yaml"
6667
expected-failure: "false"
68+
- envs: "pixi:env1"
69+
manifest-path: "envs/pixi.toml"
70+
policy-file: "policy.yaml"
71+
expected-failure: "false"
72+
- envs: |
73+
pixi:env1
74+
pixi:env2
75+
manifest-path: "envs/pixi.toml"
76+
policy-file: "policy.yaml"
77+
expected-failure: "false"
78+
- envs: |
79+
pixi:env1
80+
conda:envs/env2.yaml
81+
manifest-path: "envs/pixi.toml"
82+
policy-file: "policy.yaml"
83+
expected-failure: "false"
84+
- envs: "pixi:failing-env"
85+
manifest-path: "envs/pixi.toml"
86+
policy-file: "policy.yaml"
87+
expected-failure: "true"
6788

6889
steps:
6990
- name: clone the repository
@@ -74,8 +95,9 @@ jobs:
7495
continue-on-error: true
7596
with:
7697
policy: ${{ matrix.policy-file }}
77-
environment-paths: ${{ matrix.env-paths }}
98+
environments: ${{ matrix.envs }}
7899
today: 2024-12-20
100+
manifest-path: ${{ matrix.manifest-path }}
79101
- name: detect outcome
80102
if: always()
81103
shell: bash -l {0}

README.md

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ them to an empty mapping or sequence, respectively:
4242
ignored_violations: []
4343
```
4444

45-
Then add a new step to CI:
45+
Then add a new step to CI.
46+
47+
### conda
48+
49+
To analyze conda environments, simply pass the path to the environment file (`env.yaml`) to the `environments` key.
50+
51+
The conda environment file _must_ specify exactly the `conda-forge` channel.
4652

4753
```yaml
4854
jobs:
@@ -53,7 +59,7 @@ jobs:
5359
- uses: xarray-contrib/minimum-dependency-versions@version
5460
with:
5561
policy: policy.yaml
56-
environment-paths: path/to/env.yaml
62+
environments: path/to/env.yaml
5763
```
5864

5965
To analyze multiple environments at the same time, pass a multi-line string:
@@ -67,8 +73,63 @@ jobs:
6773
6874
- uses: xarray-contrib/minimum-dependency-versions@version
6975
with:
70-
environment-paths: |
76+
environments: |
7177
path/to/env1.yaml
7278
path/to/env2.yaml
73-
path/to/env3.yaml
79+
conda:path/to/env3.yaml # the conda: prefix is optional
80+
```
81+
82+
### pixi
83+
84+
To analyze pixi environments, specify the environment name prefixed with `pixi:` and point to the manifest file using `manifest-path`.
85+
86+
Any environment must pin the dependencies, which must be exact pins (i.e. `x.y.*` or `>=x.y.0,<x.(y + 1).0`, with the former being strongly encouraged). Lower pins are interpreted as exact pins, while all other forms of pinning are not allowed.
87+
88+
```yaml
89+
jobs:
90+
my-job:
91+
...
92+
steps:
93+
...
94+
95+
- uses: xarray-contrib/minimum-dependency-versions@version
96+
with:
97+
environments: pixi:env1
98+
manifest-path: /path/to/pixi.toml # or pyproject.toml
99+
```
100+
101+
Multiple environments can be analyzed at the same time:
102+
103+
```yaml
104+
jobs:
105+
my-job:
106+
...
107+
steps:
108+
...
109+
110+
- uses: xarray-contrib/minimum-dependency-versions@version
111+
with:
112+
environments: |
113+
pixi:env1
114+
pixi:env2
115+
manifest-path: /path/to/pixi.toml # or pyproject.toml
116+
```
117+
118+
### Mixing environment types
119+
120+
It is even possible to mix environment types (once again, the `conda:` prefix is optional but recommended):
121+
122+
```yaml
123+
jobs:
124+
my-job:
125+
...
126+
steps:
127+
...
128+
129+
- uses: xarray-contrib/minimum-dependency-versions@version
130+
with:
131+
environments: |
132+
pixi:env1
133+
conda:path/to/env.yaml
134+
manifest-path: path/to/pixi.toml # or pyproject.toml
74135
```

action.yaml

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,24 @@ inputs:
77
The path to the policy to follow
88
required: true
99
type: string
10-
environment-paths:
10+
today:
11+
description: >-
12+
Time machine for testing
13+
required: false
14+
type: string
15+
environments:
1116
description: >-
12-
The paths to the environment files
17+
The names or paths of the environments. Pixi environment names must be
18+
prefixed with `pixi:`. Conda environment paths may be prefixed with
19+
`conda:`. If there is no prefix, it is assumed to be a conda env path.
1320
required: true
1421
type: list
15-
today:
22+
manifest-path:
1623
description: >-
17-
Time machine for testing
24+
Path to the manifest file of `pixi`. Required for `pixi` environments.
1825
required: false
1926
type: string
2027
outputs: {}
21-
2228
runs:
2329
using: "composite"
2430

@@ -28,14 +34,20 @@ runs:
2834
run: |
2935
echo "::group::Install dependencies"
3036
python -m pip install -r ${{ github.action_path }}/requirements.txt
37+
python -m pip install ${{ github.action_path }}
3138
echo "::endgroup::"
3239
- name: analyze environments
3340
shell: bash -l {0}
3441
env:
3542
COLUMNS: 120
3643
FORCE_COLOR: 3
3744
POLICY_PATH: ${{ inputs.policy }}
38-
ENVIRONMENT_PATHS: ${{ inputs.environment-paths }}
45+
ENVIRONMENTS: ${{ inputs.environments }}
3946
TODAY: ${{ inputs.today }}
47+
MANIFEST_PATH: ${{ inputs.manifest-path }}
4048
run: |
41-
PYTHONPATH=${{github.action_path}} python -m minimum_versions validate --today="$TODAY" --policy="$POLICY_PATH" $ENVIRONMENT_PATHS
49+
python -m minimum_versions validate \
50+
--today="$TODAY" \
51+
--policy="$POLICY_PATH" \
52+
--manifest-path="$MANIFEST_PATH" \
53+
$ENVIRONMENTS

envs/pixi.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[dependencies]
2+
pandas = "2.1"
3+
packaging = "23.1"
4+
5+
[feature.py39.dependencies]
6+
python = "3.9"
7+
8+
[feature.py310.dependencies]
9+
python = "3.10"
10+
11+
[feature.failing.dependencies]
12+
numpy = "2.1"
13+
14+
[environments]
15+
env1 = { features = ["py310"] }
16+
env2 = ["py39"]
17+
failing-env = { features = ["failing"] }
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pathlib
2+
3+
from minimum_versions.environments.conda import parse_conda_environment
4+
from minimum_versions.environments.pixi import parse_pixi_environment
5+
from minimum_versions.environments.spec import Spec, compare_versions # noqa: F401
6+
7+
kinds = {
8+
"conda": parse_conda_environment,
9+
"pixi": parse_pixi_environment,
10+
}
11+
12+
13+
def parse_environment(specifier: str, manifest_path: pathlib.Path | None) -> list[Spec]:
14+
split = specifier.split(":", maxsplit=1)
15+
if len(split) == 1:
16+
kind = "conda"
17+
path = specifier
18+
else:
19+
kind, path = split
20+
21+
parser = kinds.get(kind)
22+
if parser is None:
23+
raise ValueError(f"Unknown kind {kind!r}, extracted from {specifier!r}.")
24+
25+
return parser(path, manifest_path)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pathlib
2+
3+
import yaml
4+
from rattler import Version
5+
6+
from minimum_versions.environments.spec import Spec
7+
8+
9+
def parse_spec(spec_text):
10+
warnings = []
11+
if ">" in spec_text or "<" in spec_text:
12+
warnings.append(
13+
f"package must be pinned with an exact version: {spec_text!r}."
14+
" Using the version as an exact pin instead."
15+
)
16+
17+
spec_text = spec_text.replace(">", "").replace("<", "")
18+
19+
if "=" in spec_text:
20+
name, version_text = spec_text.split("=", maxsplit=1)
21+
version = Version(version_text)
22+
segments = version.segments()
23+
24+
if (len(segments) == 3 and segments[2] != [0]) or len(segments) > 3:
25+
warnings.append(
26+
f"package should be pinned to a minor version (got {version})"
27+
)
28+
else:
29+
name = spec_text
30+
version = None
31+
32+
return Spec(name, version), (name, warnings)
33+
34+
35+
def parse_conda_environment(path: pathlib.Path, manifest_path: None):
36+
env = yaml.safe_load(pathlib.Path(path).read_text())
37+
38+
specs = []
39+
warnings = []
40+
for dep in env["dependencies"]:
41+
spec, warnings_ = parse_spec(dep)
42+
43+
specs.append(spec)
44+
warnings.append(warnings_)
45+
46+
return specs, warnings

0 commit comments

Comments
 (0)