Skip to content

Commit cc826a6

Browse files
authored
refactor the script to accept a policy file (xarray-contrib#23)
* refactor to read the policy from a file * add example policy with a ignored violation * expose the policy in the action * use environment variables to make the action a bit safer * add comments to the example policy * depend on jsonschema * pass the policy file in ci * pass overrides as a keyword argument * directly use the input * print the env (to debug) * more extensive debugging * typo in the policy input * rewrite the description to be more accurate * allow using a time machine (this is important to keep the tests passing) * allow passing `today` to the action * use a bash array to pass options * mock today as Sep 2024 * interpret empty string as `None` * bump the mock date * include the policy file in the readme * move the overrides into the package policy * also move `exclude` into the policy section * ignore coverage cache files * autoupdate hooks * put the dependency installation into a capture group
1 parent a2d6511 commit cc826a6

File tree

10 files changed

+192
-41
lines changed

10 files changed

+192
-41
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ jobs:
6767
id: action-run
6868
continue-on-error: true
6969
with:
70+
policy: policy.yaml
7071
environment-paths: ${{ matrix.env-paths }}
72+
today: 2024-12-20
7173
- name: detect outcome
7274
if: always()
7375
shell: bash -l {0}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.prettier_cache
22

33
__pycache__/
4+
.coverage

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v5.0.0
3+
rev: v6.0.0
44
hooks:
55
- id: trailing-whitespace
66
- id: end-of-file-fixer
@@ -9,7 +9,7 @@ repos:
99
hooks:
1010
- id: black
1111
- repo: https://github.com/astral-sh/ruff-pre-commit
12-
rev: v0.12.7
12+
rev: v0.12.11
1313
hooks:
1414
- id: ruff
1515
args: ["--fix"]

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,35 @@ Check that the minimum dependency versions follow `xarray`'s policy.
44

55
## Usage
66

7-
To use the `minimum-dependency-versions` action in workflows, simply add a new step:
7+
To use the `minimum-dependency-versions` action in workflows, create a policy file (`policy.yaml`):
8+
9+
```yaml
10+
channels:
11+
- conda-forge
12+
platforms:
13+
- noarch
14+
- linux-64
15+
policy:
16+
# policy in months
17+
# Example is xarray's values
18+
packages:
19+
python: 30
20+
numpy: 18
21+
default: 12
22+
overrides:
23+
# override the policy for specific packages
24+
package3: 0.3.1
25+
# these packages are completely ignored
26+
exclude:
27+
- package1
28+
- package2
29+
- ...
30+
# these packages don't fail the CI, but will be printed in the report as a warning
31+
ignored_violations:
32+
- package4
33+
```
34+
35+
then add a new step to CI:
836
937
```yaml
1038
jobs:
@@ -14,6 +42,7 @@ jobs:
1442
...
1543
- uses: xarray-contrib/minimum-dependency-versions@version
1644
with:
45+
policy: policy.yaml
1746
environment-paths: path/to/env.yaml
1847
```
1948

action.yaml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@ name: "minimum-dependency-versions"
22
description: >-
33
Check that the minimum dependency versions follow `xarray`'s policy.
44
inputs:
5+
policy:
6+
description: >-
7+
The path to the policy to follow
8+
required: true
9+
type: string
510
environment-paths:
611
description: >-
712
The paths to the environment files
8-
required: True
13+
required: true
914
type: list
15+
today:
16+
description: >-
17+
Time machine for testing
18+
required: false
19+
type: string
1020
outputs: {}
1121

1222
runs:
@@ -16,12 +26,16 @@ runs:
1626
- name: install dependencies
1727
shell: bash -l {0}
1828
run: |
29+
echo "::group::Install dependencies"
1930
python -m pip install -r ${{ github.action_path }}/requirements.txt
31+
echo "::endgroup::"
2032
- name: analyze environments
2133
shell: bash -l {0}
2234
env:
2335
COLUMNS: 120
2436
FORCE_COLOR: 3
25-
INPUT: ${{ inputs.environment-paths }}
37+
POLICY_PATH: ${{ inputs.policy }}
38+
ENVIRONMENT_PATHS: ${{ inputs.environment-paths }}
39+
TODAY: ${{ inputs.today }}
2640
run: |
27-
python ${{ github.action_path }}/minimum_versions.py $(echo $INPUT)
41+
python minimum_versions.py --today="$TODAY" --policy="$POLICY_PATH" $ENVIRONMENT_PATHS

envs/env2.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ dependencies:
66
- xarray=2023.10.0
77
- dask=2023.10.0
88
- distributed=2023.10.0
9+
- pydap=3.5.1

minimum_versions.py

Lines changed: 108 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
from dataclasses import dataclass, field
77

8+
import jsonschema
89
import rich_click as click
910
import yaml
1011
from dateutil.relativedelta import relativedelta
@@ -18,27 +19,62 @@
1819

1920
click.rich_click.SHOW_ARGUMENTS = True
2021

21-
channels = ["conda-forge"]
22-
platforms = ["noarch", "linux-64"]
23-
ignored_packages = [
24-
"coveralls",
25-
"hypothesis",
26-
"pip",
27-
"pytest",
28-
"pytest-cov",
29-
"pytest-env",
30-
"pytest-mypy-plugins",
31-
"pytest-timeout",
32-
"pytest-xdist",
33-
]
22+
23+
schema = {
24+
"type": "object",
25+
"properties": {
26+
"channels": {"type": "array", "items": {"type": "string"}},
27+
"platforms": {"type": "array", "items": {"type": "string"}},
28+
"policy": {
29+
"type": "object",
30+
"properties": {
31+
"packages": {
32+
"type": "object",
33+
"patternProperties": {
34+
"^[a-z][-a-z_]*$": {"type": "integer", "minimum": 1}
35+
},
36+
"additionalProperties": False,
37+
},
38+
"default": {"type": "integer", "minimum": 1},
39+
"overrides": {
40+
"type": "object",
41+
"patternProperties": {
42+
"^[a-z][-a-z_]*": {"type": "string", "format": "date"}
43+
},
44+
"additionalProperties": False,
45+
},
46+
"exclude": {"type": "array", "items": {"type": "string"}},
47+
"ignored_violations": {
48+
"type": "array",
49+
"items": {"type": "string", "pattern": "^[a-z][-a-z_]*$"},
50+
},
51+
},
52+
"required": [
53+
"packages",
54+
"default",
55+
"overrides",
56+
"exclude",
57+
"ignored_violations",
58+
],
59+
},
60+
},
61+
"required": ["channels", "platforms", "policy"],
62+
}
3463

3564

3665
@dataclass
3766
class Policy:
3867
package_months: dict
3968
default_months: int
69+
70+
channels: list[str] = field(default_factory=list)
71+
platforms: list[str] = field(default_factory=list)
72+
4073
overrides: dict[str, Version] = field(default_factory=dict)
4174

75+
ignored_violations: list[str] = field(default_factory=list)
76+
exclude: list[str] = field(default_factory=list)
77+
4278
def minimum_version(self, today, package_name, releases):
4379
if (override := self.overrides.get(package_name)) is not None:
4480
return find_release(releases, version=override)
@@ -117,6 +153,28 @@ def parse_environment(text):
117153
return specs, warnings
118154

119155

156+
def parse_policy(file):
157+
policy = yaml.safe_load(file)
158+
try:
159+
jsonschema.validate(instance=policy, schema=schema)
160+
except jsonschema.ValidationError as e:
161+
raise jsonschema.ValidationError(
162+
f"Invalid policy definition: {str(e)}"
163+
) from None
164+
165+
package_policy = policy["policy"]
166+
167+
return Policy(
168+
channels=policy["channels"],
169+
platforms=policy["platforms"],
170+
exclude=package_policy["exclude"],
171+
package_months=package_policy["packages"],
172+
default_months=package_policy["default"],
173+
ignored_violations=package_policy["ignored_violations"],
174+
overrides=package_policy["overrides"],
175+
)
176+
177+
120178
def is_preview(version):
121179
candidates = {"rc", "b", "a"}
122180

@@ -175,11 +233,15 @@ def lookup_spec_release(spec, releases):
175233
return releases[spec.name][version]
176234

177235

178-
def compare_versions(environments, policy_versions):
236+
def compare_versions(environments, policy_versions, ignored_violations):
179237
status = {}
180238
for env, specs in environments.items():
181239
env_status = any(
182-
spec.version > policy_versions[spec.name].version for spec in specs
240+
(
241+
spec.name not in ignored_violations
242+
and spec.version > policy_versions[spec.name].version
243+
)
244+
for spec in specs
183245
)
184246
status[env] = env_status
185247
return status
@@ -194,7 +256,7 @@ def version_comparison_symbol(required, policy):
194256
return "="
195257

196258

197-
def format_bump_table(specs, policy_versions, releases, warnings):
259+
def format_bump_table(specs, policy_versions, releases, warnings, ignored_violations):
198260
table = Table(
199261
Column("Package", width=20),
200262
Column("Required", width=8),
@@ -221,7 +283,10 @@ def format_bump_table(specs, policy_versions, releases, warnings):
221283
required_date = lookup_spec_release(spec, releases).timestamp
222284

223285
status = version_comparison_symbol(required_version, policy_version)
224-
style = styles[status]
286+
if status == ">" and spec.name in ignored_violations:
287+
style = warning_style
288+
else:
289+
style = styles[status]
225290

226291
table.add_row(
227292
spec.name,
@@ -255,15 +320,26 @@ def format_bump_table(specs, policy_versions, releases, warnings):
255320
return grid
256321

257322

323+
def parse_date(string):
324+
if not string:
325+
return None
326+
327+
return datetime.datetime.strptime(string, "%Y-%m-%d").date()
328+
329+
258330
@click.command()
259331
@click.argument(
260332
"environment_paths",
261333
type=click.Path(exists=True, readable=True, path_type=pathlib.Path),
262334
nargs=-1,
263335
)
264-
def main(environment_paths):
336+
@click.option("--today", type=parse_date, default=None)
337+
@click.option("--policy", "policy_file", type=click.File(mode="r"), required=True)
338+
def main(today, policy_file, environment_paths):
265339
console = Console()
266340

341+
policy = parse_policy(policy_file)
342+
267343
parsed_environments = {
268344
path.stem: parse_environment(path.read_text()) for path in environment_paths
269345
}
@@ -272,30 +348,22 @@ def main(environment_paths):
272348
env: dict(warnings_) for env, (_, warnings_) in parsed_environments.items()
273349
}
274350
environments = {
275-
env: [spec for spec in specs if spec.name not in ignored_packages]
351+
env: [spec for spec in specs if spec.name not in policy.exclude]
276352
for env, (specs, _) in parsed_environments.items()
277353
}
278354

279355
all_packages = list(
280356
dict.fromkeys(spec.name for spec in concat(environments.values()))
281357
)
282358

283-
policy_months = {
284-
"python": 30,
285-
"numpy": 18,
286-
}
287-
policy_months_default = 12
288-
overrides = {}
289-
290-
policy = Policy(
291-
policy_months, default_months=policy_months_default, overrides=overrides
292-
)
293-
294359
gateway = Gateway()
295-
query = gateway.query(channels, platforms, all_packages, recursive=False)
360+
query = gateway.query(
361+
policy.channels, policy.platforms, all_packages, recursive=False
362+
)
296363
records = asyncio.run(query)
297364

298-
today = datetime.date.today()
365+
if today is None:
366+
today = datetime.date.today()
299367
package_releases = pipe(
300368
records,
301369
concat,
@@ -307,13 +375,19 @@ def main(environment_paths):
307375
package_releases,
308376
curry(find_policy_versions, policy, today),
309377
)
310-
status = compare_versions(environments, policy_versions)
378+
status = compare_versions(environments, policy_versions, policy.ignored_violations)
311379

312380
release_lookup = {
313381
n: {r.version: r for r in releases} for n, releases in package_releases.items()
314382
}
315383
grids = {
316-
env: format_bump_table(specs, policy_versions, release_lookup, warnings[env])
384+
env: format_bump_table(
385+
specs,
386+
policy_versions,
387+
release_lookup,
388+
warnings[env],
389+
policy.ignored_violations,
390+
)
317391
for env, specs in environments.items()
318392
}
319393
root_grid = Table.grid()

policy.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
channels:
2+
- conda-forge
3+
platforms:
4+
- noarch
5+
- linux-64
6+
policy:
7+
# all packages in months
8+
packages:
9+
python: 30
10+
numpy: 18
11+
default: 12
12+
# overrides for the policy
13+
overrides: {}
14+
# these packages are completely ignored
15+
exclude:
16+
- coveralls
17+
- pip
18+
- pytest
19+
- pytest-asyncio
20+
- pytest-cov
21+
- pytest-env
22+
- pytest-mypy-plugins
23+
- pytest-timeout
24+
- pytest-xdist
25+
- pytest-hypothesis
26+
# these packages don't fail the CI, but will be printed in the report
27+
ignored_violations:
28+
- pydap

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ rich-click
44
cytoolz
55
pyyaml
66
python-dateutil
7+
jsonschema
8+
rfc3339-validator

0 commit comments

Comments
 (0)