From 4b211300748ba96371452e8eaf45fff612994635 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 13:11:34 +0100 Subject: [PATCH 01/49] import the script --- minimum_versions.py | 323 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 minimum_versions.py diff --git a/minimum_versions.py b/minimum_versions.py new file mode 100644 index 0000000..c226e30 --- /dev/null +++ b/minimum_versions.py @@ -0,0 +1,323 @@ +import asyncio +import bisect +import datetime +import pathlib +import sys +from dataclasses import dataclass, field + +import rich_click as click +import yaml +from dateutil.relativedelta import relativedelta +from rattler import Gateway, Version +from rich.console import Console +from rich.panel import Panel +from rich.style import Style +from rich.table import Column, Table +from tlz.functoolz import curry, pipe +from tlz.itertoolz import concat, groupby + +click.rich_click.SHOW_ARGUMENTS = True + +channels = ["conda-forge"] +platforms = ["noarch", "linux-64"] +ignored_packages = [ + "coveralls", + "pip", + "pytest", + "pytest-cov", + "pytest-env", + "pytest-xdist", + "pytest-timeout", + "hypothesis", +] + + +@dataclass +class Policy: + package_months: dict + default_months: int + overrides: dict[str, Version] = field(default_factory=dict) + + def minimum_version(self, package_name, releases): + if (override := self.overrides.get(package_name)) is not None: + return override + + policy_months = self.package_months.get(package_name, self.default_months) + today = datetime.date.today() + + cutoff_date = today - relativedelta(months=policy_months) + + index = bisect.bisect_left( + releases, cutoff_date, key=lambda x: x.timestamp.date() + ) + return releases[index - 1 if index > 0 else 0] + + +@dataclass +class Spec: + name: str + version: Version | None + + @classmethod + def parse(cls, spec_text): + warnings = [] + if ">" in spec_text or "<" in spec_text: + warnings.append( + f"package should be pinned with an exact version: {spec_text!r}" + ) + + spec_text = spec_text.replace(">", "").replace("<", "") + + if "=" in spec_text: + name, version_text = spec_text.split("=", maxsplit=1) + version = Version(version_text) + segments = version.segments() + + if len(segments) != 2 or (len(segments) == 3 and segments[2] != 0): + warnings.append( + f"package should be pinned to a minor version (got {version})" + ) + else: + name = spec_text + version = None + + return cls(name, version), (name, warnings) + + +@dataclass(order=True) +class Release: + version: Version + build_number: int + timestamp: datetime.datetime = field(compare=False) + + @classmethod + def from_repodata_record(cls, repo_data): + return cls( + version=repo_data.version, + build_number=repo_data.build_number, + timestamp=repo_data.timestamp, + ) + + +def parse_environment(text): + env = yaml.safe_load(text) + + specs = [] + warnings = [] + for dep in env["dependencies"]: + spec, warnings_ = Spec.parse(dep) + + warnings.append(warnings_) + specs.append(spec) + + return specs, warnings + + +def is_preview(version): + candidates = ["rc", "beta", "alpha"] + + *_, last_segment = version.segments() + return any(candidate in last_segment for candidate in candidates) + + +def group_packages(records): + groups = groupby(lambda r: r.name.normalized, records) + return { + name: sorted(map(Release.from_repodata_record, group)) + for name, group in groups.items() + } + + +def filter_releases(predicate, releases): + return { + name: [r for r in records if predicate(r)] for name, records in releases.items() + } + + +def deduplicate_releases(package_info): + def deduplicate(releases): + return min(releases, key=lambda p: p.timestamp) + + return { + name: list(map(deduplicate, groupby(lambda p: p.version, group).values())) + for name, group in package_info.items() + } + + +def find_policy_versions(policy, releases): + return { + name: policy.minimum_version(name, package_releases) + for name, package_releases in releases.items() + } + + +def is_suitable_release(release): + if release.timestamp is None: + return False + + segments = release.version.extend_to_length(3).segments() + + return segments[2] == [0] + + +def lookup_spec_release(spec, releases): + version = spec.version.extend_to_length(3) + + return releases[spec.name][version] + + +def compare_versions(environments, policy_versions): + status = {} + for env, specs in environments.items(): + env_status = any( + spec.version > policy_versions[spec.name].version for spec in specs + ) + status[env] = env_status + return status + + +def version_comparison_symbol(required, policy): + if required < policy: + return "<" + elif required > policy: + return ">" + else: + return "=" + + +def format_bump_table(specs, policy_versions, releases, warnings): + table = Table( + Column("Package", width=20), + Column("Required", width=8), + "Required (date)", + Column("Policy", width=8), + "Policy (date)", + "Status", + ) + + heading_style = Style(color="#ff0000", bold=True) + warning_style = Style(color="#ffff00", bold=True) + styles = { + ">": Style(color="#ff0000", bold=True), + "=": Style(color="#008700", bold=True), + "<": Style(color="#d78700", bold=True), + } + + for spec in specs: + policy_release = policy_versions[spec.name] + policy_version = policy_release.version.with_segments(0, 2) + policy_date = policy_release.timestamp + + required_version = spec.version + required_date = lookup_spec_release(spec, releases).timestamp + + status = version_comparison_symbol(required_version, policy_version) + style = styles[status] + + table.add_row( + spec.name, + str(required_version), + f"{required_date:%Y-%m-%d}", + str(policy_version), + f"{policy_date:%Y-%m-%d}", + status, + style=style, + ) + + grid = Table.grid(expand=True, padding=(0, 2)) + grid.add_column(style=heading_style, vertical="middle") + grid.add_column() + grid.add_row("Version summary", table) + + if any(warnings.values()): + warning_table = Table(width=table.width, expand=True) + warning_table.add_column("Package") + warning_table.add_column("Warning") + + for package, messages in warnings.items(): + if not messages: + continue + warning_table.add_row(package, messages[0], style=warning_style) + for message in messages[1:]: + warning_table.add_row("", message, style=warning_style) + + grid.add_row("Warnings", warning_table) + + return grid + + +@click.command() +@click.argument( + "environment_paths", + type=click.Path(exists=True, readable=True, path_type=pathlib.Path), + nargs=-1, +) +def main(environment_paths): + console = Console() + + parsed_environments = { + path.stem: parse_environment(path.read_text()) for path in environment_paths + } + + warnings = { + env: dict(warnings_) for env, (_, warnings_) in parsed_environments.items() + } + environments = { + env: [spec for spec in specs if spec.name not in ignored_packages] + for env, (specs, _) in parsed_environments.items() + } + + all_packages = list( + dict.fromkeys(spec.name for spec in concat(environments.values())) + ) + + policy_months = { + "python": 30, + "numpy": 18, + } + policy_months_default = 12 + overrides = {} + + policy = Policy( + policy_months, default_months=policy_months_default, overrides=overrides + ) + + gateway = Gateway() + query = gateway.query(channels, platforms, all_packages, recursive=False) + records = asyncio.run(query) + + package_releases = pipe( + records, + concat, + group_packages, + curry(filter_releases, lambda r: r.timestamp is not None), + deduplicate_releases, + ) + policy_versions = pipe( + package_releases, + curry(filter_releases, is_suitable_release), + curry(find_policy_versions, policy), + ) + status = compare_versions(environments, policy_versions) + + release_lookup = { + n: {r.version: r for r in releases} for n, releases in package_releases.items() + } + grids = { + env: format_bump_table(specs, policy_versions, release_lookup, warnings[env]) + for env, specs in environments.items() + } + root_grid = Table.grid() + root_grid.add_column() + + for env, grid in grids.items(): + root_grid.add_row(Panel(grid, title=env, expand=True)) + + console.print(root_grid) + + status_code = 1 if any(status.values()) else 0 + sys.exit(status_code) + + +if __name__ == "__main__": + main() From 3d9eff3980f724810ce77315ca38364c463cc652 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 13:11:40 +0100 Subject: [PATCH 02/49] list the dependencies --- requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3a72cd4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +py-rattler +rich +rich-click +cytoolz +pyyaml +python-dateutil From 3b4c18906ac03df85f9bd32f48f952b5ff26069e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 13:15:04 +0100 Subject: [PATCH 03/49] call the script from within the action --- action.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 286fb6e..ae83c7e 100644 --- a/action.yaml +++ b/action.yaml @@ -10,4 +10,11 @@ outputs: {} runs: using: "composite" - steps: {} + steps: + - name: install dependencies + run: | + python -m pip install -r requirements.txt + shell: bash -l {0} + - name: analyze environments + run: | + python minimum_versions.py ${{ inputs.env-paths }} From 416fbfeb8a20bce68b46943443271eeae168932e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 13:17:20 +0100 Subject: [PATCH 04/49] explicitly specify the shell --- action.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index ae83c7e..d569656 100644 --- a/action.yaml +++ b/action.yaml @@ -12,9 +12,10 @@ runs: using: "composite" steps: - name: install dependencies + shell: bash -l {0} run: | python -m pip install -r requirements.txt - shell: bash -l {0} - name: analyze environments + shell: bash -l {0} run: | python minimum_versions.py ${{ inputs.env-paths }} From d398fac196d062ac22008eaeaa791397830ca7d8 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 13:20:06 +0100 Subject: [PATCH 05/49] quote the `envs-path` variable --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf26143..28d52ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,4 +48,4 @@ jobs: - name: run action uses: ./ with: - environment-paths: ${{ matrix.env-paths }} + environment-paths: "${{ matrix.env-paths }}" From 9f59968459c668947abe336108fd741a27a7ed77 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 15:07:02 +0100 Subject: [PATCH 06/49] try setting the input type of env-paths --- action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yaml b/action.yaml index d569656..a60b641 100644 --- a/action.yaml +++ b/action.yaml @@ -6,6 +6,7 @@ inputs: description: >- The paths to the environment files required: True + type: list outputs: {} runs: From c0f36141d2e8059da0de9f2b07de533c49b72c6a Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 15:11:10 +0100 Subject: [PATCH 07/49] try serializing to json --- .github/workflows/ci.yml | 2 +- action.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28d52ca..200b009 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,4 +48,4 @@ jobs: - name: run action uses: ./ with: - environment-paths: "${{ matrix.env-paths }}" + environment-paths: "${{ toJson(matrix.env-paths) }}" diff --git a/action.yaml b/action.yaml index a60b641..42a9eb8 100644 --- a/action.yaml +++ b/action.yaml @@ -6,7 +6,7 @@ inputs: description: >- The paths to the environment files required: True - type: list + type: string outputs: {} runs: @@ -19,4 +19,4 @@ runs: - name: analyze environments shell: bash -l {0} run: | - python minimum_versions.py ${{ inputs.env-paths }} + python minimum_versions.py ${{ fromJson(inputs.env-paths) }} From 670a6234d3bbe4080d5806c4bad5404f7a88ea61 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 15:13:45 +0100 Subject: [PATCH 08/49] quote the input --- action.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 42a9eb8..884a7c7 100644 --- a/action.yaml +++ b/action.yaml @@ -16,7 +16,10 @@ runs: shell: bash -l {0} run: | python -m pip install -r requirements.txt + - name: input paths + run: | + echo "${{ fromJson(inputs.env-paths) }}" - name: analyze environments shell: bash -l {0} run: | - python minimum_versions.py ${{ fromJson(inputs.env-paths) }} + python minimum_versions.py "${{ fromJson(inputs.env-paths) }}" From 80652c1cc820bbcc3faaba818a8346f3bf7561a3 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 15:14:29 +0100 Subject: [PATCH 09/49] missing shell --- action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yaml b/action.yaml index 884a7c7..04d8ab4 100644 --- a/action.yaml +++ b/action.yaml @@ -17,6 +17,7 @@ runs: run: | python -m pip install -r requirements.txt - name: input paths + shell: bash -l {0} run: | echo "${{ fromJson(inputs.env-paths) }}" - name: analyze environments From 70b826cde342c079fef17aff1e3f3de0bc878b5c Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 15:15:53 +0100 Subject: [PATCH 10/49] correct the input name --- action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.yaml b/action.yaml index 04d8ab4..eed4680 100644 --- a/action.yaml +++ b/action.yaml @@ -19,8 +19,8 @@ runs: - name: input paths shell: bash -l {0} run: | - echo "${{ fromJson(inputs.env-paths) }}" + echo "${{ inputs.environment-paths }}" - name: analyze environments shell: bash -l {0} run: | - python minimum_versions.py "${{ fromJson(inputs.env-paths) }}" + python minimum_versions.py "${{ fromJson(inputs.environment-paths) }}" From 23fcf8290ef9290f96cdc5e8bdaf0d9ff814922e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 15:16:26 +0100 Subject: [PATCH 11/49] print the decoded paths --- action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yaml b/action.yaml index eed4680..4973618 100644 --- a/action.yaml +++ b/action.yaml @@ -20,6 +20,7 @@ runs: shell: bash -l {0} run: | echo "${{ inputs.environment-paths }}" + echo "${{ fromJson(input.environment-paths) }}" - name: analyze environments shell: bash -l {0} run: | From 689c049d5c62bc37406a87052e6f0e1672e3df46 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 15:17:08 +0100 Subject: [PATCH 12/49] typo --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 4973618..82ab096 100644 --- a/action.yaml +++ b/action.yaml @@ -20,7 +20,7 @@ runs: shell: bash -l {0} run: | echo "${{ inputs.environment-paths }}" - echo "${{ fromJson(input.environment-paths) }}" + echo "${{ fromJson(inputs.environment-paths) }}" - name: analyze environments shell: bash -l {0} run: | From 5773e7c4127a8cd94474a20e73d32bde07a9a75c Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 15:21:49 +0100 Subject: [PATCH 13/49] use the right function names --- .github/workflows/ci.yml | 2 +- action.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 200b009..39b0982 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,4 +48,4 @@ jobs: - name: run action uses: ./ with: - environment-paths: "${{ toJson(matrix.env-paths) }}" + environment-paths: "${{ toJSON(matrix.env-paths) }}" diff --git a/action.yaml b/action.yaml index 82ab096..09b781c 100644 --- a/action.yaml +++ b/action.yaml @@ -20,8 +20,8 @@ runs: shell: bash -l {0} run: | echo "${{ inputs.environment-paths }}" - echo "${{ fromJson(inputs.environment-paths) }}" + echo "${{ fromJSON(inputs.environment-paths) }}" - name: analyze environments shell: bash -l {0} run: | - python minimum_versions.py "${{ fromJson(inputs.environment-paths) }}" + python minimum_versions.py "${{ fromJSON(inputs.environment-paths) }}" From a2b494ce7ca505ad698667194f1b74a42d0750c1 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:15:37 +0100 Subject: [PATCH 14/49] unquote --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 09b781c..490fb19 100644 --- a/action.yaml +++ b/action.yaml @@ -24,4 +24,4 @@ runs: - name: analyze environments shell: bash -l {0} run: | - python minimum_versions.py "${{ fromJSON(inputs.environment-paths) }}" + python minimum_versions.py ${{ fromJSON(inputs.environment-paths) }} From 03b5e25d1e978d6790cf507c04ea6fa58cacc741 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:25:23 +0100 Subject: [PATCH 15/49] try using `join` --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 490fb19..5669073 100644 --- a/action.yaml +++ b/action.yaml @@ -24,4 +24,4 @@ runs: - name: analyze environments shell: bash -l {0} run: | - python minimum_versions.py ${{ fromJSON(inputs.environment-paths) }} + python minimum_versions.py ${{ join(fromJSON(inputs.environment-paths), ' ') }} From 1f48cd0fa3407ac8a75dcc5fa7987f15cd0b9c8c Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:26:24 +0100 Subject: [PATCH 16/49] remove debug step --- action.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/action.yaml b/action.yaml index 5669073..6b97dfb 100644 --- a/action.yaml +++ b/action.yaml @@ -16,11 +16,6 @@ runs: shell: bash -l {0} run: | python -m pip install -r requirements.txt - - name: input paths - shell: bash -l {0} - run: | - echo "${{ inputs.environment-paths }}" - echo "${{ fromJSON(inputs.environment-paths) }}" - name: analyze environments shell: bash -l {0} run: | From 9094fb203d3c6fd69170d10fd021f4a97fde1a1b Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:46:15 +0100 Subject: [PATCH 17/49] allow expected failures --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39b0982..cfe94c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,10 @@ jobs: - ["envs/env1.yaml"] - ["envs/env2.yaml"] - ["envs/env1.yaml", "envs/env2.yaml"] + expected-failure: false + include: + - env-paths: ["envs/failing-env1.yaml"] + expected-failure: true steps: - name: clone the repository @@ -49,3 +53,12 @@ jobs: uses: ./ with: environment-paths: "${{ toJSON(matrix.env-paths) }}" + - name: evaluate failures + if: | + always() + && ( + (failure() && ! ${{ matrix.expected-failure }}) + || (success() && ${{ matrix.expected-failure }}) + ) + shell: bash -l {0} + run: exit 1 From f823a9a5d3427b9f8614a78d5d77782c0ded869a Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:48:48 +0100 Subject: [PATCH 18/49] add test env files --- envs/env1.yml | 7 +++++++ envs/env2.yaml | 8 ++++++++ envs/failing-env1.yaml | 6 ++++++ 3 files changed, 21 insertions(+) create mode 100644 envs/env1.yml create mode 100644 envs/env2.yaml create mode 100644 envs/failing-env1.yaml diff --git a/envs/env1.yml b/envs/env1.yml new file mode 100644 index 0000000..d9b0b97 --- /dev/null +++ b/envs/env1.yml @@ -0,0 +1,7 @@ +channels: + - conda-forge +dependencies: + - python=3.10 + - numpy=1.24 + - pandas=2.1 + - packaging=23.4 diff --git a/envs/env2.yaml b/envs/env2.yaml new file mode 100644 index 0000000..8ac6e2d --- /dev/null +++ b/envs/env2.yaml @@ -0,0 +1,8 @@ +channels: + - conda-forge +dependencies: + - python=3.10 + - numpy=1.23 + - xarray=2023.10.0 + - dask=2023.10.0 + - distributed=2023.10.0 diff --git a/envs/failing-env1.yaml b/envs/failing-env1.yaml new file mode 100644 index 0000000..a2c1889 --- /dev/null +++ b/envs/failing-env1.yaml @@ -0,0 +1,6 @@ +channels: + - conda-forge +dependencies: + - python=3.11 + - numpy=2.1 + - pandas=2.3.1 From 56649b6c18501aef020fe2e7e173d0578a9e190f Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:52:20 +0100 Subject: [PATCH 19/49] compare strings instead --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfe94c7..051f59e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,10 +41,10 @@ jobs: - ["envs/env1.yaml"] - ["envs/env2.yaml"] - ["envs/env1.yaml", "envs/env2.yaml"] - expected-failure: false + expected-failure: "false" include: - env-paths: ["envs/failing-env1.yaml"] - expected-failure: true + expected-failure: "true" steps: - name: clone the repository @@ -57,8 +57,8 @@ jobs: if: | always() && ( - (failure() && ! ${{ matrix.expected-failure }}) - || (success() && ${{ matrix.expected-failure }}) + (failure() && ${{ matrix.expected-failure }} == "false") + || (success() && ${{ matrix.expected-failure }} == "true") ) shell: bash -l {0} run: exit 1 From 2b90a42c90af7439c13524c341bafd9639af0bac Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:54:06 +0100 Subject: [PATCH 20/49] pass expected failure as a list --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 051f59e..6e17360 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - ["envs/env1.yaml"] - ["envs/env2.yaml"] - ["envs/env1.yaml", "envs/env2.yaml"] - expected-failure: "false" + expected-failure: ["false"] include: - env-paths: ["envs/failing-env1.yaml"] expected-failure: "true" From 5bb582c0dcf13bd4161ea34db4372a100bd54bd2 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:55:44 +0100 Subject: [PATCH 21/49] another failing example --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e17360..a570a12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,8 @@ jobs: include: - env-paths: ["envs/failing-env1.yaml"] expected-failure: "true" + - env-paths: ["envs/env1.yaml", "envs/failing-env1.yaml"] + expected-failure: "true" steps: - name: clone the repository From a45662b99e36d1109ee61d8f8c2154ac83bce739 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:57:56 +0100 Subject: [PATCH 22/49] show the status of the current dir --- action.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/action.yaml b/action.yaml index 6b97dfb..26813bf 100644 --- a/action.yaml +++ b/action.yaml @@ -16,6 +16,12 @@ runs: shell: bash -l {0} run: | python -m pip install -r requirements.txt + - name: show files + shell: bash -l {0} + run: | + pwd + ls -l + ls -l ${{ join(fromJSON(inputs.environment-paths), ' ') }} - name: analyze environments shell: bash -l {0} run: | From b470c76e7887c7977f4e4294a1383611f928ac1f Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 16:59:20 +0100 Subject: [PATCH 23/49] more debugging --- action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yaml b/action.yaml index 26813bf..12d5c6c 100644 --- a/action.yaml +++ b/action.yaml @@ -21,6 +21,7 @@ runs: run: | pwd ls -l + ls -l envs/env1.yaml ls -l ${{ join(fromJSON(inputs.environment-paths), ' ') }} - name: analyze environments shell: bash -l {0} From 2866cda51a86d0139acd010062e82b34320fc89e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:00:07 +0100 Subject: [PATCH 24/49] print the content of `envs` --- action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yaml b/action.yaml index 12d5c6c..4347083 100644 --- a/action.yaml +++ b/action.yaml @@ -22,6 +22,7 @@ runs: pwd ls -l ls -l envs/env1.yaml + ls -l envs/ ls -l ${{ join(fromJSON(inputs.environment-paths), ' ') }} - name: analyze environments shell: bash -l {0} From 906112e0e612be8070cc5caabe8deb8d0f8f240d Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:00:58 +0100 Subject: [PATCH 25/49] rename `env1` --- envs/{env1.yml => env1.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename envs/{env1.yml => env1.yaml} (100%) diff --git a/envs/env1.yml b/envs/env1.yaml similarity index 100% rename from envs/env1.yml rename to envs/env1.yaml From 4c0f2d4a42c7147072b8dcc406a60c6369ac9f34 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:02:55 +0100 Subject: [PATCH 26/49] use a existing version of `pandas` --- envs/failing-env1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/failing-env1.yaml b/envs/failing-env1.yaml index a2c1889..2022866 100644 --- a/envs/failing-env1.yaml +++ b/envs/failing-env1.yaml @@ -3,4 +3,4 @@ channels: dependencies: - python=3.11 - numpy=2.1 - - pandas=2.3.1 + - pandas=2.2.1 From 211a60915da2ace7e9d55fb245c245c1efdf37aa Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:04:52 +0100 Subject: [PATCH 27/49] use an existing version of `packaging` --- envs/env1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/env1.yaml b/envs/env1.yaml index d9b0b97..9ab3f67 100644 --- a/envs/env1.yaml +++ b/envs/env1.yaml @@ -4,4 +4,4 @@ dependencies: - python=3.10 - numpy=1.24 - pandas=2.1 - - packaging=23.4 + - packaging=23.1 From 132fef76f7efd476f6fa2a3aea8efd2f92eff831 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:07:22 +0100 Subject: [PATCH 28/49] enforce a column width of 120 --- action.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/action.yaml b/action.yaml index 4347083..fcac84a 100644 --- a/action.yaml +++ b/action.yaml @@ -11,6 +11,9 @@ outputs: {} runs: using: "composite" + + env: COLUMNS=120 + steps: - name: install dependencies shell: bash -l {0} From 16ba62b9e83fee47a3e63b82cffa9a6ff5f4dc59 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:09:49 +0100 Subject: [PATCH 29/49] proper syntax --- action.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index fcac84a..b2b7bf4 100644 --- a/action.yaml +++ b/action.yaml @@ -12,7 +12,8 @@ outputs: {} runs: using: "composite" - env: COLUMNS=120 + env: + COLUMNS: 120 steps: - name: install dependencies From 7cd86c158d56721d24c44162ef2693f7720a08ee Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:13:37 +0100 Subject: [PATCH 30/49] try setting in a different place --- action.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/action.yaml b/action.yaml index b2b7bf4..9bf655e 100644 --- a/action.yaml +++ b/action.yaml @@ -12,9 +12,6 @@ outputs: {} runs: using: "composite" - env: - COLUMNS: 120 - steps: - name: install dependencies shell: bash -l {0} @@ -30,5 +27,7 @@ runs: ls -l ${{ join(fromJSON(inputs.environment-paths), ' ') }} - name: analyze environments shell: bash -l {0} + env: + COLUMNS: 120 run: | python minimum_versions.py ${{ join(fromJSON(inputs.environment-paths), ' ') }} From f35f7a6e9ed5bb4d479d0f13aff58e2ee9160991 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:13:54 +0100 Subject: [PATCH 31/49] remove debug step --- action.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/action.yaml b/action.yaml index 9bf655e..662194c 100644 --- a/action.yaml +++ b/action.yaml @@ -17,14 +17,6 @@ runs: shell: bash -l {0} run: | python -m pip install -r requirements.txt - - name: show files - shell: bash -l {0} - run: | - pwd - ls -l - ls -l envs/env1.yaml - ls -l envs/ - ls -l ${{ join(fromJSON(inputs.environment-paths), ' ') }} - name: analyze environments shell: bash -l {0} env: From d2afd5da7bfc238ecf7946fdf20ac7a236c20c20 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:16:33 +0100 Subject: [PATCH 32/49] enforce colors --- action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yaml b/action.yaml index 662194c..f9f852a 100644 --- a/action.yaml +++ b/action.yaml @@ -21,5 +21,6 @@ runs: shell: bash -l {0} env: COLUMNS: 120 + FORCE_COLOR: 3 run: | python minimum_versions.py ${{ join(fromJSON(inputs.environment-paths), ' ') }} From 51b720922f18376504f0e2ce64f2376c5aee7774 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 17:17:11 +0100 Subject: [PATCH 33/49] remove the `always` --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a570a12..e4aaa5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,10 +57,9 @@ jobs: environment-paths: "${{ toJSON(matrix.env-paths) }}" - name: evaluate failures if: | - always() - && ( - (failure() && ${{ matrix.expected-failure }} == "false") - || (success() && ${{ matrix.expected-failure }} == "true") + ( + (failure() && ${{ matrix.expected-failure }} != "true") + || (success() && ${{ matrix.expected-failure }} != "false") ) shell: bash -l {0} run: exit 1 From 88a99f21981c4d78522e8843efdddb0787cb9c7d Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:01:45 +0100 Subject: [PATCH 34/49] split up into smaller steps --- .github/workflows/ci.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4aaa5b..bd2df0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,11 +55,19 @@ jobs: uses: ./ with: environment-paths: "${{ toJSON(matrix.env-paths) }}" - - name: evaluate failures + - name: detect unexpected failures if: | - ( - (failure() && ${{ matrix.expected-failure }} != "true") - || (success() && ${{ matrix.expected-failure }} != "false") - ) + failure() && ${{ matrix.expected-failure }} == "false" shell: bash -l {0} - run: exit 1 + run: | + echo "STATUS=1" >> $GITHUB_ENV + - name: detect unexpected passes + if: | + success() && ${{ matrix.expected-failure }} == "true" + shell: bash -l {0} + run: | + echo "STATUS=0" >> $GITHUB_ENV + - name: evaluate status + shell: bash -l {0} + run: | + exit ${{ env.STATUS }} From 9d16bbe891f8a987c1ea2d190e1f8baf3330c4d5 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:03:32 +0100 Subject: [PATCH 35/49] don't cancel in-progress jobs when others fail --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd2df0e..7c10df6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: name: tests runs-on: [ubuntu-latest] strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] @@ -36,6 +37,7 @@ jobs: runs-on: [ubuntu-latest] strategy: + fail-fast: false matrix: env-paths: - ["envs/env1.yaml"] From 613b72ec2ac38022204d53e3a12371e94f1382ce Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:04:02 +0100 Subject: [PATCH 36/49] cancel duplicate workflow runs --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c10df6..a123755 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,10 @@ on: pull_request: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: ci: name: tests From a4c96e115a3de636258007c047ee4e2be9f10f99 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:07:13 +0100 Subject: [PATCH 37/49] continue if the action fails --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a123755..d23e302 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,7 @@ jobs: uses: actions/checkout@v4 - name: run action uses: ./ + continue-on-error: true with: environment-paths: "${{ toJSON(matrix.env-paths) }}" - name: detect unexpected failures From e99ab75cd4b0674b02d3ef76e77ad015688bf7a1 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:10:32 +0100 Subject: [PATCH 38/49] more output --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d23e302..c2b4240 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,18 +62,24 @@ jobs: continue-on-error: true with: environment-paths: "${{ toJSON(matrix.env-paths) }}" + - name: setup status + shell: bash -l {0} + run: | + echo "STATUS=0" >> $GITHUB_ENV - name: detect unexpected failures if: | failure() && ${{ matrix.expected-failure }} == "false" shell: bash -l {0} run: | + echo "unexpected failure" echo "STATUS=1" >> $GITHUB_ENV - name: detect unexpected passes if: | success() && ${{ matrix.expected-failure }} == "true" shell: bash -l {0} run: | - echo "STATUS=0" >> $GITHUB_ENV + echo "unexpected pass" + echo "STATUS=2" >> $GITHUB_ENV - name: evaluate status shell: bash -l {0} run: | From 1854f7eeb8d9274376b635c4b575791013612062 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:28:30 +0100 Subject: [PATCH 39/49] tests for `Spec.parse` --- minimum_versions.py | 2 +- test_script.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 test_script.py diff --git a/minimum_versions.py b/minimum_versions.py index c226e30..7d69953 100644 --- a/minimum_versions.py +++ b/minimum_versions.py @@ -73,7 +73,7 @@ def parse(cls, spec_text): version = Version(version_text) segments = version.segments() - if len(segments) != 2 or (len(segments) == 3 and segments[2] != 0): + if (len(segments) == 3 and segments[2] != [0]) or len(segments) > 3: warnings.append( f"package should be pinned to a minor version (got {version})" ) diff --git a/test_script.py b/test_script.py new file mode 100644 index 0000000..ab7577f --- /dev/null +++ b/test_script.py @@ -0,0 +1,25 @@ +import pytest +from rattler import Version + +from minimum_versions import Spec + + +@pytest.mark.parametrize( + ["text", "expected_spec", "expected_name", "expected_warnings"], + ( + ("numpy=1.23", Spec("numpy", Version("1.23")), "numpy", []), + ("xarray=2024.10.0", Spec("xarray", Version("2024.10.0")), "xarray", []), + ( + "xarray=2024.10.1", + Spec("xarray", Version("2024.10.1")), + "xarray", + ["package should be pinned to a minor version (got 2024.10.1)"], + ), + ), +) +def test_spec_parse(text, expected_spec, expected_name, expected_warnings): + actual_spec, (actual_name, actual_warnings) = Spec.parse(text) + + assert actual_spec == expected_spec + assert actual_name == expected_name + assert actual_warnings == expected_warnings From 6663062768cae45f9b676d694ca708e5280e804f Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:29:11 +0100 Subject: [PATCH 40/49] always set the pass status --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2b4240..23e29bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,10 +59,10 @@ jobs: uses: actions/checkout@v4 - name: run action uses: ./ - continue-on-error: true with: environment-paths: "${{ toJSON(matrix.env-paths) }}" - name: setup status + if: always() shell: bash -l {0} run: | echo "STATUS=0" >> $GITHUB_ENV From 359658c3d4b7fba98ca80f7e83c14b1993220292 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:33:18 +0100 Subject: [PATCH 41/49] check the output of the status functions --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23e29bd..74c6788 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,7 @@ jobs: if: always() shell: bash -l {0} run: | + echo "${{ success() }} vs ${{ failure() }}" echo "STATUS=0" >> $GITHUB_ENV - name: detect unexpected failures if: | From 3f227785bc2306158b978125e5b79185811b4c67 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:41:34 +0100 Subject: [PATCH 42/49] consolidate into a single step --- .github/workflows/ci.yml | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74c6788..c567118 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,29 +59,28 @@ jobs: uses: actions/checkout@v4 - name: run action uses: ./ + id: action-run with: environment-paths: "${{ toJSON(matrix.env-paths) }}" - - name: setup status + - name: detect outcome if: always() shell: bash -l {0} run: | - echo "${{ success() }} vs ${{ failure() }}" - echo "STATUS=0" >> $GITHUB_ENV - - name: detect unexpected failures - if: | - failure() && ${{ matrix.expected-failure }} == "false" - shell: bash -l {0} - run: | - echo "unexpected failure" - echo "STATUS=1" >> $GITHUB_ENV - - name: detect unexpected passes - if: | - success() && ${{ matrix.expected-failure }} == "true" - shell: bash -l {0} - run: | - echo "unexpected pass" - echo "STATUS=2" >> $GITHUB_ENV - - name: evaluate status - shell: bash -l {0} - run: | + if [[ "${{ steps.action-run.outcome }}" == "success" && ${{ matrix.expected-failure }} == "true" ]]; then + # unexpected failure + echo "workflow xpassed" + echo "STATUS=1" >> $GITHUB_ENV + elif [[ "${{ steps.action-run.outcome }}" == "failure" && ${{ matrix.expected-failure }} == "false" ]]; then + # unexpected pass + echo "workflow failed" + echo "STATUS=2" >> $GITHUB_ENV + elif [[ "${{ steps.action-run.outcome }}" == "success" ]]; then + # normal pass + echo "passed" + echo "STATUS=0" >> $GITHUB_ENV + else + # cancelled + echo "workflow cancelled" + echo "STATUS=3" >> $GITHUB_ENV + fi exit ${{ env.STATUS }} From f8632e91848e70e75fabc663530153659d12b735 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:42:58 +0100 Subject: [PATCH 43/49] continue on error --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c567118..a097aa0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,7 @@ jobs: - name: run action uses: ./ id: action-run + continue-on-error: true with: environment-paths: "${{ toJSON(matrix.env-paths) }}" - name: detect outcome From 8aac73135c9e268a8f58d64cc8993664a38e16f3 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:49:13 +0100 Subject: [PATCH 44/49] some more adjustments --- .github/workflows/ci.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a097aa0..2e672b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,20 +68,24 @@ jobs: shell: bash -l {0} run: | if [[ "${{ steps.action-run.outcome }}" == "success" && ${{ matrix.expected-failure }} == "true" ]]; then - # unexpected failure + # unexpected pass echo "workflow xpassed" - echo "STATUS=1" >> $GITHUB_ENV + export STATUS=1 elif [[ "${{ steps.action-run.outcome }}" == "failure" && ${{ matrix.expected-failure }} == "false" ]]; then - # unexpected pass + # unexpected failure echo "workflow failed" - echo "STATUS=2" >> $GITHUB_ENV - elif [[ "${{ steps.action-run.outcome }}" == "success" ]]; then + export STATUS=2 + elif [[ "${{ steps.action-run.outcome }}" == "success" && ${{ matrix.expected-failure }} == "false" ]]; then + # normal pass + echo "workflow passed" + export STATUS=0 + elif [[ "${{ steps.action-run.outcome }}" == "success" && ${{ matrix.expected-failure }} == "true" ]]; then # normal pass - echo "passed" - echo "STATUS=0" >> $GITHUB_ENV + echo "workflow xfailed" + export STATUS=0 else # cancelled echo "workflow cancelled" - echo "STATUS=3" >> $GITHUB_ENV + export STATUS=3 fi - exit ${{ env.STATUS }} + exit $STATUS From 3ccaf95791a26784a98b23158d06905bc836f60d Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 18:53:12 +0100 Subject: [PATCH 45/49] fix the `xfail` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e672b5..d3d05b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: # normal pass echo "workflow passed" export STATUS=0 - elif [[ "${{ steps.action-run.outcome }}" == "success" && ${{ matrix.expected-failure }} == "true" ]]; then + elif [[ "${{ steps.action-run.outcome }}" == "failure" && ${{ matrix.expected-failure }} == "true" ]]; then # normal pass echo "workflow xfailed" export STATUS=0 From 2ba79be7a6b4ab25a878ff50424f4a29891b2bac Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 11 Nov 2024 22:52:48 +0100 Subject: [PATCH 46/49] Apply suggestions from code review Co-authored-by: Mathias Hauser --- .github/workflows/ci.yml | 2 +- minimum_versions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3d05b8..a99cedf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: echo "workflow passed" export STATUS=0 elif [[ "${{ steps.action-run.outcome }}" == "failure" && ${{ matrix.expected-failure }} == "true" ]]; then - # normal pass + # expected failure echo "workflow xfailed" export STATUS=0 else diff --git a/minimum_versions.py b/minimum_versions.py index 7d69953..735d154 100644 --- a/minimum_versions.py +++ b/minimum_versions.py @@ -107,8 +107,8 @@ def parse_environment(text): for dep in env["dependencies"]: spec, warnings_ = Spec.parse(dep) - warnings.append(warnings_) specs.append(spec) + warnings.append(warnings_) return specs, warnings From 46b13bc4f34fdbb44f12a39e35f775eb4fa495e6 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Fri, 15 Nov 2024 16:03:55 +0100 Subject: [PATCH 47/49] expect pypi-compliant versions --- minimum_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minimum_versions.py b/minimum_versions.py index 735d154..c528f3d 100644 --- a/minimum_versions.py +++ b/minimum_versions.py @@ -114,7 +114,7 @@ def parse_environment(text): def is_preview(version): - candidates = ["rc", "beta", "alpha"] + candidates = {"rc", "b", "a"} *_, last_segment = version.segments() return any(candidate in last_segment for candidate in candidates) From 16ce1772cfe476882f6b66831f1611af9b188409 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Tue, 12 Nov 2024 17:55:55 +0100 Subject: [PATCH 48/49] extend the inexact versions warning --- minimum_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minimum_versions.py b/minimum_versions.py index c528f3d..832d922 100644 --- a/minimum_versions.py +++ b/minimum_versions.py @@ -63,7 +63,7 @@ def parse(cls, spec_text): warnings = [] if ">" in spec_text or "<" in spec_text: warnings.append( - f"package should be pinned with an exact version: {spec_text!r}" + f"package must be pinned with an exact version: {spec_text!r}. Using the version as an exact pin instead." ) spec_text = spec_text.replace(">", "").replace("<", "") From 1812a2516c693e017306789ebbf9bcd00596aeff Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 18 Nov 2024 21:59:59 +0100 Subject: [PATCH 49/49] add and fix tests for `Policy.minimum_version` --- minimum_versions.py | 26 +++++++++++++++-------- test_script.py | 51 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/minimum_versions.py b/minimum_versions.py index 832d922..321537a 100644 --- a/minimum_versions.py +++ b/minimum_versions.py @@ -38,19 +38,22 @@ class Policy: default_months: int overrides: dict[str, Version] = field(default_factory=dict) - def minimum_version(self, package_name, releases): + def minimum_version(self, today, package_name, releases): if (override := self.overrides.get(package_name)) is not None: - return override + return find_release(releases, version=override) + + suitable_releases = [ + release for release in releases if is_suitable_release(release) + ] policy_months = self.package_months.get(package_name, self.default_months) - today = datetime.date.today() cutoff_date = today - relativedelta(months=policy_months) index = bisect.bisect_left( - releases, cutoff_date, key=lambda x: x.timestamp.date() + suitable_releases, cutoff_date, key=lambda x: x.timestamp.date() ) - return releases[index - 1 if index > 0 else 0] + return suitable_releases[index - 1 if index > 0 else 0] @dataclass @@ -134,6 +137,11 @@ def filter_releases(predicate, releases): } +def find_release(releases, version): + index = bisect.bisect_left(releases, version, key=lambda x: x.version) + return releases[index] + + def deduplicate_releases(package_info): def deduplicate(releases): return min(releases, key=lambda p: p.timestamp) @@ -144,9 +152,9 @@ def deduplicate(releases): } -def find_policy_versions(policy, releases): +def find_policy_versions(policy, today, releases): return { - name: policy.minimum_version(name, package_releases) + name: policy.minimum_version(today, name, package_releases) for name, package_releases in releases.items() } @@ -286,6 +294,7 @@ def main(environment_paths): query = gateway.query(channels, platforms, all_packages, recursive=False) records = asyncio.run(query) + today = datetime.date.today() package_releases = pipe( records, concat, @@ -295,8 +304,7 @@ def main(environment_paths): ) policy_versions = pipe( package_releases, - curry(filter_releases, is_suitable_release), - curry(find_policy_versions, policy), + curry(find_policy_versions, policy, today), ) status = compare_versions(environments, policy_versions) diff --git a/test_script.py b/test_script.py index ab7577f..32d4db3 100644 --- a/test_script.py +++ b/test_script.py @@ -1,7 +1,9 @@ +import datetime as dt + import pytest from rattler import Version -from minimum_versions import Spec +from minimum_versions import Policy, Release, Spec @pytest.mark.parametrize( @@ -23,3 +25,50 @@ def test_spec_parse(text, expected_spec, expected_name, expected_warnings): assert actual_spec == expected_spec assert actual_name == expected_name assert actual_warnings == expected_warnings + + +@pytest.mark.parametrize( + ["package_name", "policy", "today", "expected"], + ( + ( + "numpy", + Policy({"numpy": 6}, 12, {}), + dt.date(2023, 12, 12), + Release(Version("1.23.0"), 0, dt.datetime(2023, 6, 9)), + ), + ( + "scipy", + Policy({"numpy": 6}, 8, {}), + dt.date(2024, 9, 5), + Release(Version("1.2.0"), 0, dt.datetime(2024, 1, 3)), + ), + ( + "scipy", + Policy({"numpy": 6}, 8, {"scipy": Version("1.1.1")}), + dt.date(2024, 9, 5), + Release(Version("1.1.1"), 0, dt.datetime(2023, 12, 1)), + ), + ), +) +def test_policy_minimum_version(package_name, policy, today, expected): + releases = { + "numpy": [ + Release(Version("1.22.0"), 0, dt.datetime(2022, 12, 1)), + Release(Version("1.22.1"), 0, dt.datetime(2023, 2, 5)), + Release(Version("1.23.0"), 0, dt.datetime(2023, 6, 9)), + Release(Version("1.23.1"), 0, dt.datetime(2023, 8, 12)), + Release(Version("1.23.2"), 0, dt.datetime(2023, 12, 5)), + ], + "scipy": [ + Release(Version("1.0.0"), 0, dt.datetime(2022, 11, 10)), + Release(Version("1.0.1"), 0, dt.datetime(2023, 1, 13)), + Release(Version("1.1.0"), 0, dt.datetime(2023, 9, 21)), + Release(Version("1.1.1"), 0, dt.datetime(2023, 12, 1)), + Release(Version("1.2.0"), 0, dt.datetime(2024, 1, 3)), + Release(Version("1.2.1"), 0, dt.datetime(2024, 2, 5)), + ], + } + + actual = policy.minimum_version(today, package_name, releases[package_name]) + + assert actual == expected