Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ Checklist:

- [ ] I added a summary of any user-facing changes (compared to the last release) in the `changelog-entries/<PRnumber>.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`).
28 changes: 28 additions & 0 deletions .github/workflows/check-requirements-reference.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions changelog-entries/610.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions tools/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <python_bindings_or_fenics_image> 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:

Expand Down
11 changes: 11 additions & 0 deletions tools/tests/requirements-reference.txt
Original file line number Diff line number Diff line change
@@ -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
116 changes: 116 additions & 0 deletions tools/tests/update_requirements_reference.py
Original file line number Diff line number Diff line change
@@ -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()
53 changes: 53 additions & 0 deletions tools/tests/validate_requirements_reference.py
Original file line number Diff line number Diff line change
@@ -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())