diff --git a/.github/ci/min-core-deps.yml b/.github/ci/min-core-deps.yml deleted file mode 100644 index 55c3f47a6..000000000 --- a/.github/ci/min-core-deps.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: xarray-tests -channels: - - conda-forge - - nodefaults -dependencies: - # MINIMUM VERSIONS POLICY: keep track of minimum versions - # for core packages. Dev and conda release builds should use this as reference. - # Run ci/min_deps_check.py to verify that this file respects the policy. - - python=3.11 - - cftime=1.6 - - dask=2022.8 - - matplotlib-base=3.5 - # netcdf follows a 1.major.minor[.patch] convention - # (see https://github.com/Unidata/netcdf4-python/issues/1090) - - netcdf4=1.6 - - numpy=1.23 - - pytest=7.1 - - scipy=1.9 - - tqdm=4.64 - - xarray=2022.6 - - zarr=2.12 diff --git a/.github/ci/min_deps_check.py b/.github/ci/min_deps_check.py deleted file mode 100644 index 36278ff3c..000000000 --- a/.github/ci/min_deps_check.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python -"""Fetch from conda database all available versions of dependencies and their -publication date. Compare it against requirements/min-core-deps.yml to verify the -policy on obsolete dependencies is being followed. Print a pretty report :) - -Adapted from xarray: -https://github.com/pydata/xarray/blob/a04d857a03d1fb04317d636a7f23239cb9034491/ci/min_deps_check.py - -See licenses/XARRAY_LICENSE for license details. -""" - -from __future__ import annotations - -import itertools -import sys -from collections.abc import Iterator -from datetime import datetime - -import conda.api # type: ignore[import] -import yaml -from dateutil.relativedelta import relativedelta - -CHANNELS = ["conda-forge", "defaults"] -IGNORE_DEPS = {} - -POLICY_MONTHS = {"python": 3 * 12} -POLICY_MONTHS_DEFAULT = 24 -POLICY_OVERRIDE: dict[str, tuple[int, int]] = {} -errors = [] - - -def error(msg: str) -> None: - global errors - errors.append(msg) - print("ERROR:", msg) - - -def warning(msg: str) -> None: - print("WARNING:", msg) - - -def parse_requirements(fname) -> Iterator[tuple[str, int, int, int | None]]: - """Load requirements/min-all-deps.yml - - Yield (package name, major version, minor version, [patch version]) - """ - global errors - - with open(fname) as fh: - contents = yaml.safe_load(fh) - for row in contents["dependencies"]: - if isinstance(row, dict) and list(row) == ["pip"]: - continue - pkg, eq, version = row.partition("=") - if pkg.rstrip("<>") in IGNORE_DEPS: - continue - if pkg.endswith("<") or pkg.endswith(">") or eq != "=": - error("package should be pinned with exact version: " + row) - continue - - try: - version_tup = tuple(int(x) for x in version.split(".")) - except ValueError as e: - raise ValueError("non-numerical version: " + row) from e - - if len(version_tup) == 2: - yield (pkg, *version_tup, None) # type: ignore[misc] - elif len(version_tup) == 3: - yield (pkg, *version_tup) # type: ignore[misc] - else: - raise ValueError("expected major.minor or major.minor.patch: " + row) - - -def query_conda(pkg: str) -> dict[tuple[int, int], datetime]: - """Query the conda repository for a specific package - - Return map of {(major version, minor version): publication date} - """ - - def metadata(entry): - version = entry.version - - time = datetime.fromtimestamp(entry.timestamp) - major, minor = map(int, version.split(".")[:2]) - - return (major, minor), time - - raw_data = conda.api.SubdirData.query_all(pkg, channels=CHANNELS) - data = sorted(metadata(entry) for entry in raw_data if entry.timestamp != 0) - - release_dates = { - version: [time for _, time in group if time is not None] - for version, group in itertools.groupby(data, key=lambda x: x[0]) - } - out = {version: min(dates) for version, dates in release_dates.items() if dates} - - # Hardcoded fix to work around incorrect dates in conda - if pkg == "python": - out.update( - { - (2, 7): datetime(2010, 6, 3), - (3, 5): datetime(2015, 9, 13), - (3, 6): datetime(2016, 12, 23), - (3, 7): datetime(2018, 6, 27), - (3, 8): datetime(2019, 10, 14), - (3, 9): datetime(2020, 10, 5), - (3, 10): datetime(2021, 10, 4), - (3, 11): datetime(2022, 10, 24), - } - ) - - return out - - -def process_pkg(pkg: str, req_major: int, req_minor: int, req_patch: int | None) -> tuple[str, str, str, str, str, str]: - """Compare package version from requirements file to available versions in conda. - Return row to build pandas dataframe: - - - package name - - major.minor.[patch] version in requirements file - - publication date of version in requirements file (YYYY-MM-DD) - - major.minor version suggested by policy - - publication date of version suggested by policy (YYYY-MM-DD) - - status ("<", "=", "> (!)") - """ - print(f"Analyzing {pkg}...") - versions = query_conda(pkg) - - try: - req_published = versions[req_major, req_minor] - except KeyError: - error("not found in conda: " + pkg) - return pkg, fmt_version(req_major, req_minor, req_patch), "-", "-", "-", "(!)" - - policy_months = POLICY_MONTHS.get(pkg, POLICY_MONTHS_DEFAULT) - policy_published = datetime.now() - relativedelta(months=policy_months) - - filtered_versions = [version for version, published in versions.items() if published < policy_published] - policy_major, policy_minor = max(filtered_versions, default=(req_major, req_minor)) - - try: - policy_major, policy_minor = POLICY_OVERRIDE[pkg] - except KeyError: - pass - policy_published_actual = versions[policy_major, policy_minor] - - if (req_major, req_minor) < (policy_major, policy_minor): - status = "<" - elif (req_major, req_minor) > (policy_major, policy_minor): - status = "> (!)" - delta = relativedelta(datetime.now(), req_published).normalized() - n_months = delta.years * 12 + delta.months - warning( - f"Package is too new: {pkg}={req_major}.{req_minor} was " - f"published on {req_published:%Y-%m-%d} " - f"which was {n_months} months ago (policy is {policy_months} months)" - ) - else: - status = "=" - - if req_patch is not None: - warning("patch version should not appear in requirements file: " + pkg) - status += " (w)" - - return ( - pkg, - fmt_version(req_major, req_minor, req_patch), - req_published.strftime("%Y-%m-%d"), - fmt_version(policy_major, policy_minor), - policy_published_actual.strftime("%Y-%m-%d"), - status, - ) - - -def fmt_version(major: int, minor: int, patch: int | None = None) -> str: - if patch is None: - return f"{major}.{minor}" - else: - return f"{major}.{minor}.{patch}" - - -def main() -> None: - fname = sys.argv[1] - rows = [process_pkg(pkg, major, minor, patch) for pkg, major, minor, patch in parse_requirements(fname)] - - print("\nPackage Required Policy Status") - print("----------------- -------------------- -------------------- ------") - fmt = "{:17} {:7} ({:10}) {:7} ({:10}) {}" - for row in rows: - print(fmt.format(*row)) - - if errors: - print("\nErrors:") - print("-------") - for i, e in enumerate(errors): - print(f"{i + 1}. {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/.github/ci/policy.yaml b/.github/ci/policy.yaml new file mode 100644 index 000000000..2d7b6f2e5 --- /dev/null +++ b/.github/ci/policy.yaml @@ -0,0 +1,25 @@ +channels: + - conda-forge +platforms: + - noarch + - linux-64 +policy: + # all packages in months + packages: + python: 30 + numpy: 18 + default: 12 + # overrides for the policy + overrides: {} + # these packages are completely ignored + exclude: + - pytest + - hypothesis + - pytest-html + - pytest-cov + - nbval + - uxarray # undergoes active development + - xgcm # undergoes active development + + # these packages don't fail the CI, but will be printed in the report + ignored_violations: [] diff --git a/.github/workflows/additional.yml b/.github/workflows/additional.yml index f9d914c09..a06a7a540 100644 --- a/.github/workflows/additional.yml +++ b/.github/workflows/additional.yml @@ -8,32 +8,21 @@ jobs: min-version-policy: name: Minimum Version Policy runs-on: "ubuntu-latest" - defaults: - run: - shell: bash -l {0} - + env: + COLUMNS: 120 steps: - uses: actions/checkout@v5 - - name: Setup micromamba - uses: mamba-org/setup-micromamba@v2 - with: - environment-name: min-deps - create-args: >- - python=3.12 - pyyaml - conda - python-dateutil - - - name: Core deps minimum versions policy - run: | - python .github/ci/min_deps_check.py .github/ci/min-core-deps.yml + - uses: astral-sh/setup-uv@v7 + - run: | + uv run --with 'minimum-dependency-versions @ git+https://github.com/xarray-contrib/minimum-dependency-versions@v1.0.0' \ + python -m minimum_versions validate \ + --policy=.github/ci/policy.yaml \ + --manifest-path=./pixi.toml \ + pixi:test-minimum linkcheck: name: pixi run docs-linkcheck runs-on: "ubuntu-latest" - defaults: - run: - shell: bash -l {0} steps: - uses: actions/checkout@v5 diff --git a/pixi.toml b/pixi.toml index f8998126a..633ce8cf1 100644 --- a/pixi.toml +++ b/pixi.toml @@ -19,7 +19,7 @@ setuptools_scm = "*" [package.run-dependencies] # keep section in sync with pyproject.toml dependencies python = ">=3.11" -netcdf4 = ">=1.7.2" +netcdf4 = ">=1.6.0" numpy = ">=2.1.0" tqdm = ">=4.50.0" xarray = ">=2024.5.0" @@ -35,18 +35,18 @@ pooch = ">=1.8.0" parcels = { path = "." } [feature.minimum.dependencies] -python = "==3.11" -netcdf4 = "==1.7.2" -numpy = "==2.1.0" -tqdm = "==4.50.0" -xarray = "==2024.5.0" -uxarray = "==2025.3.0" -dask = "==2024.5.1" -zarr = "==2.15.0" -xgcm = "==0.9.0" -cf_xarray = "==0.8.6" -cftime = "==1.6.3" -pooch = "==1.8.0" +python = "3.11.*" +netcdf4 = "1.6.*" +numpy = "2.1.*" +tqdm = "4.50.*" +xarray = "2024.5.*" +uxarray = "2025.3.*" +dask = "2024.5.*" +zarr = "2.15.*" +xgcm = "0.9.*" +cf_xarray = "0.8.*" +cftime = "1.6.*" +pooch = "1.8.*" [feature.py311.dependencies] python = "3.11.*"