From b88bfcc81c73729596c29781e915908e724fb097 Mon Sep 17 00:00:00 2001 From: PranjalManhgaye Date: Mon, 9 Mar 2026 00:22:32 +0530 Subject: [PATCH] Add requirements-reference.txt for reproducible Python deps (fixes #610) - Add tools/tests/requirements-reference.txt with pinned versions - Add update_requirements_reference.py to regenerate from reference_versions.yaml - Add validate_requirements_reference.py to check pyprecice matches - Add GitHub Action check-requirements-reference.yml - Document in tools/tests/README.md and release PR template Made-with: Cursor --- .github/pull_request_template.md | 2 + .../check-requirements-reference.yml | 28 +++++ changelog-entries/610.md | 1 + tools/tests/README.md | 19 +++ tools/tests/requirements-reference.txt | 11 ++ tools/tests/update_requirements_reference.py | 116 ++++++++++++++++++ .../tests/validate_requirements_reference.py | 53 ++++++++ 7 files changed, 230 insertions(+) create mode 100644 .github/workflows/check-requirements-reference.yml create mode 100644 changelog-entries/610.md create mode 100644 tools/tests/requirements-reference.txt create mode 100644 tools/tests/update_requirements_reference.py create mode 100644 tools/tests/validate_requirements_reference.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 96c288ce0..50c46f5b1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,3 +6,5 @@ Checklist: - [ ] I added a summary of any user-facing changes (compared to the last release) in the `changelog-entries/.md`. - [ ] I will remember to squash-and-merge, providing a useful summary of the changes of this PR. + +For **release PRs** (new distribution): update `tools/tests/requirements-reference.txt` if `reference_versions.yaml` changed (`python3 tools/tests/update_requirements_reference.py`). diff --git a/.github/workflows/check-requirements-reference.yml b/.github/workflows/check-requirements-reference.yml new file mode 100644 index 000000000..15874dcda --- /dev/null +++ b/.github/workflows/check-requirements-reference.yml @@ -0,0 +1,28 @@ +name: Check requirements-reference +on: + push: + branches: + - master + - develop + paths: + - tools/tests/requirements-reference.txt + - tools/tests/reference_versions.yaml + - tools/tests/update_requirements_reference.py + pull_request: + branches: + - master + - develop + paths: + - tools/tests/requirements-reference.txt + - tools/tests/reference_versions.yaml + - tools/tests/update_requirements_reference.py +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Validate requirements-reference + run: python3 tools/tests/validate_requirements_reference.py diff --git a/changelog-entries/610.md b/changelog-entries/610.md new file mode 100644 index 000000000..dc7b62a95 --- /dev/null +++ b/changelog-entries/610.md @@ -0,0 +1 @@ +- Add `requirements-reference.txt` (lockfile-style pinned Python versions) and `update_requirements_reference.py` for reproducible dependency versions per distribution (fixes [#610](https://github.com/precice/tutorials/issues/610)). Includes GitHub Action to validate pyprecice version matches `reference_versions.yaml`; release PR template reminder to update the reference file. diff --git a/tools/tests/README.md b/tools/tests/README.md index 5675c47b6..fca84976b 100644 --- a/tools/tests/README.md +++ b/tools/tests/README.md @@ -183,6 +183,25 @@ User-facing tools: - `print_case_combinations.py`: Prints all possible combinations of tutorial cases, using the `metadata.yaml` files. - `build_docker_images.py`: Build the Docker images for each test - `generate_reference_results.py`: Executes the system tests with the versions defined in `reference_versions.yaml` and generates the reference data archives, with the names described in `tests.yaml`. (should only be used by the CI Pipeline) + - `update_requirements_reference.py`: Regenerates `requirements-reference.txt` with pinned Python versions from `reference_versions.yaml` (for reproducibility, see issue #610). + - `validate_requirements_reference.py`: Validates that `requirements-reference.txt` exists and pyprecice version matches `reference_versions.yaml`. + +### requirements-reference.txt + +A lockfile-style list of pinned Python dependency versions (pyprecice, numpy, matplotlib, nutils, setuptools) for reproducible builds and distributions (see issue [#610](https://github.com/precice/tutorials/issues/610)). This file is a **reference manifest only**: tutorial `run.sh` scripts keep using their own `requirements.txt` (with loose constraints) to avoid merge back-and-forth; system tests use the Docker image’s venv. The reference file records “versions known to work” for a distribution. + +**Update at each release.** For best accuracy (match what CI uses), capture from the systemtest Docker image: + +```bash +docker run --rm pip freeze | python3 update_requirements_reference.py --from-freeze +``` + +Or regenerate from `reference_versions.yaml` only (pyprecice from PYTHON_BINDINGS_REF; others from script defaults): + +```bash +cd tools/tests +python3 update_requirements_reference.py +``` Implementation scripts: diff --git a/tools/tests/requirements-reference.txt b/tools/tests/requirements-reference.txt new file mode 100644 index 000000000..9094a61c1 --- /dev/null +++ b/tools/tests/requirements-reference.txt @@ -0,0 +1,11 @@ +# Pinned Python dependency versions for reproducible system tests and distributions. +# Generated from reference_versions.yaml (PYTHON_BINDINGS_REF). Update at each release. +# Run: python3 update_requirements_reference.py +# +# See tools/tests/README.md for how to update this file. + +matplotlib==3.9.0 +numpy==1.26.4 +nutils==7.2 +pyprecice==3.2.0 +setuptools>=69.0.0 diff --git a/tools/tests/update_requirements_reference.py b/tools/tests/update_requirements_reference.py new file mode 100644 index 000000000..e6c6a2ae0 --- /dev/null +++ b/tools/tests/update_requirements_reference.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Update requirements-reference.txt with pinned versions from reference_versions.yaml. + +This script reads PYTHON_BINDINGS_REF from reference_versions.yaml and generates +a requirements-reference.txt with pyprecice pinned to the corresponding version. +Other common packages (numpy, matplotlib, setuptools) use fixed versions known +to work with the tutorials. + +Run from tools/tests/: + python3 update_requirements_reference.py + +Or to regenerate from a pip freeze (e.g. from Docker): + pip freeze | python3 update_requirements_reference.py --from-freeze +""" +import argparse +import re +import sys +from pathlib import Path + +REFERENCE_VERSIONS = Path(__file__).parent / "reference_versions.yaml" +REQUIREMENTS_REF = Path(__file__).parent / "requirements-reference.txt" + +# Default pinned versions for common packages (fallback when not using --from-freeze) +DEFAULTS = { + "matplotlib": "3.9.0", + "numpy": "1.26.4", + "nutils": "7.2", + "pyprecice": None, # From reference_versions.yaml + "setuptools": ">=69.0.0", +} + +# Packages to include (in order) +PACKAGES = ["matplotlib", "numpy", "nutils", "pyprecice", "setuptools"] + + +def get_pyprecice_version_from_ref(ref: str) -> str: + """Convert PYTHON_BINDINGS_REF (e.g. v3.2.0) to pyprecice version (3.2.0).""" + return ref.lstrip("v").strip() + + +def load_reference_versions() -> str: + """Load PYTHON_BINDINGS_REF from reference_versions.yaml.""" + text = REFERENCE_VERSIONS.read_text() + for line in text.splitlines(): + if "PYTHON_BINDINGS_REF" in line and ":" in line: + match = re.search(r'["\']([^"\']+)["\']', line) + if match: + return match.group(1) + raise ValueError("PYTHON_BINDINGS_REF not found in reference_versions.yaml") + + +def parse_freezed_packages(freezed: str) -> dict[str, str]: + """Parse pip freeze output into {package: version}.""" + result = {} + for line in freezed.strip().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if "==" in line: + pkg, ver = line.split("==", 1) + result[pkg.lower()] = f"=={ver.strip()}" + elif "===" in line: + pkg, ver = line.split("===", 1) + result[pkg.lower()] = f"=={ver.strip()}" + return result + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Update requirements-reference.txt from reference_versions.yaml" + ) + parser.add_argument( + "--from-freeze", + action="store_true", + help="Read pip freeze from stdin and use those versions for known packages", + ) + args = parser.parse_args() + + pyprecice_ref = load_reference_versions() + pyprecice_ver = get_pyprecice_version_from_ref(pyprecice_ref) + + if args.from_freeze: + freezed = parse_freezed_packages(sys.stdin.read()) + versions = {} + for pkg in PACKAGES: + if pkg.lower() in freezed: + versions[pkg] = freezed[pkg.lower()] + elif pkg == "pyprecice": + versions[pkg] = f"=={pyprecice_ver}" + elif DEFAULTS.get(pkg): + versions[pkg] = ( + DEFAULTS[pkg] if DEFAULTS[pkg].startswith(("==", ">=", "~=")) else f"=={DEFAULTS[pkg]}" + ) + else: + DEFAULTS["pyprecice"] = pyprecice_ver + versions = { + pkg: f"=={ver}" if ver and not ver.startswith(("==", ">=", "~=")) else (ver or "") + for pkg, ver in DEFAULTS.items() + } + versions["pyprecice"] = f"=={pyprecice_ver}" + + header = """# Pinned Python dependency versions for reproducible system tests and distributions. +# Generated from reference_versions.yaml (PYTHON_BINDINGS_REF). Update at each release. +# Run: python3 update_requirements_reference.py +# +# See tools/tests/README.md for how to update this file. + +""" + lines = [f"{pkg}{versions.get(pkg, '')}\n" for pkg in PACKAGES if pkg in versions] + REQUIREMENTS_REF.write_text(header + "".join(lines)) + print(f"Wrote {REQUIREMENTS_REF} (pyprecice from {pyprecice_ref})") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/validate_requirements_reference.py b/tools/tests/validate_requirements_reference.py new file mode 100644 index 000000000..870f53b8c --- /dev/null +++ b/tools/tests/validate_requirements_reference.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Validate that requirements-reference.txt exists and pyprecice version +matches PYTHON_BINDINGS_REF in reference_versions.yaml. + +Exit 0 on success, 1 on failure. +""" +import re +import sys +from pathlib import Path + +TOOLS_TESTS = Path(__file__).parent +REFERENCE_VERSIONS = TOOLS_TESTS / "reference_versions.yaml" +REQUIREMENTS_REF = TOOLS_TESTS / "requirements-reference.txt" + + +def main() -> int: + if not REQUIREMENTS_REF.exists(): + print(f"ERROR: {REQUIREMENTS_REF} not found. Run update_requirements_reference.py.", file=sys.stderr) + return 1 + + # Load PYTHON_BINDINGS_REF + ref_text = REFERENCE_VERSIONS.read_text() + ref_match = re.search(r'PYTHON_BINDINGS_REF:\s*["\']([^"\']+)["\']', ref_text) + if not ref_match: + print("ERROR: PYTHON_BINDINGS_REF not found in reference_versions.yaml", file=sys.stderr) + return 1 + + expected_ver = ref_match.group(1).lstrip("v").strip() + + # Parse pyprecice from requirements-reference.txt + req_text = REQUIREMENTS_REF.read_text() + precice_match = re.search(r"pyprecice\s*==\s*([\w.]+)", req_text) + if not precice_match: + print("ERROR: pyprecice not found in requirements-reference.txt", file=sys.stderr) + return 1 + + actual_ver = precice_match.group(1).strip() + if actual_ver != expected_ver: + print( + f"ERROR: pyprecice version mismatch: requirements-reference.txt has {actual_ver}, " + f"reference_versions.yaml PYTHON_BINDINGS_REF has {ref_match.group(1)}. " + "Run: python3 update_requirements_reference.py", + file=sys.stderr, + ) + return 1 + + print(f"OK: requirements-reference.txt pyprecice=={actual_ver} matches reference_versions.yaml") + return 0 + + +if __name__ == "__main__": + sys.exit(main())