Skip to content

Commit 19bd1cb

Browse files
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
1 parent 0f34006 commit 19bd1cb

7 files changed

Lines changed: 231 additions & 1 deletion

File tree

.github/pull_request_template.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ TODO
1010
- Add a [sidebar entry](https://github.com/precice/precice.github.io/blob/master/_data/sidebars/tutorial_sidebar.yml)
1111
- Add it to the [overview](https://github.com/precice/precice.github.io/blob/master/content/tutorials/tutorials.md)
1212

13+
For **release PRs** (new distribution): update `tools/tests/requirements-reference.txt` if `reference_versions.yaml` changed (`python3 tools/tests/update_requirements_reference.py`).
14+
1315
## Resources
1416

1517
- [Contributing tutorials](https://precice.org/community-contribute-to-precice.html#contributing-tutorials)
16-
- [System tests documentation](https://precice.org/dev-docs-system-tests.html)
18+
- [System tests documentation](https://precice.org/dev-docs-system-tests.html)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Check requirements-reference
2+
on:
3+
push:
4+
branches:
5+
- master
6+
- develop
7+
paths:
8+
- tools/tests/requirements-reference.txt
9+
- tools/tests/reference_versions.yaml
10+
- tools/tests/update_requirements_reference.py
11+
pull_request:
12+
branches:
13+
- master
14+
- develop
15+
paths:
16+
- tools/tests/requirements-reference.txt
17+
- tools/tests/reference_versions.yaml
18+
- tools/tests/update_requirements_reference.py
19+
jobs:
20+
validate:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
- uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.10"
27+
- name: Validate requirements-reference
28+
run: python3 tools/tests/validate_requirements_reference.py

changelog-entries/610.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

tools/tests/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,25 @@ User-facing tools:
185185
- `print_case_combinations.py`: Prints all possible combinations of tutorial cases, using the `metadata.yaml` files.
186186
- `build_docker_images.py`: Build the Docker images for each test
187187
- `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)
188+
- `update_requirements_reference.py`: Regenerates `requirements-reference.txt` with pinned Python versions from `reference_versions.yaml` (for reproducibility, see issue #610).
189+
- `validate_requirements_reference.py`: Validates that `requirements-reference.txt` exists and pyprecice version matches `reference_versions.yaml`.
190+
191+
### requirements-reference.txt
192+
193+
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.
194+
195+
**Update at each release.** For best accuracy (match what CI uses), capture from the systemtest Docker image:
196+
197+
```bash
198+
docker run --rm <python_bindings_or_fenics_image> pip freeze | python3 update_requirements_reference.py --from-freeze
199+
```
200+
201+
Or regenerate from `reference_versions.yaml` only (pyprecice from PYTHON_BINDINGS_REF; others from script defaults):
202+
203+
```bash
204+
cd tools/tests
205+
python3 update_requirements_reference.py
206+
```
188207
189208
Implementation scripts:
190209
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Pinned Python dependency versions for reproducible system tests and distributions.
2+
# Generated from reference_versions.yaml (PYTHON_BINDINGS_REF). Update at each release.
3+
# Run: python3 update_requirements_reference.py
4+
#
5+
# See tools/tests/README.md for how to update this file.
6+
7+
matplotlib==3.9.0
8+
numpy==1.26.4
9+
nutils==7.2
10+
pyprecice==3.2.0
11+
setuptools>=69.0.0
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Update requirements-reference.txt with pinned versions from reference_versions.yaml.
4+
5+
This script reads PYTHON_BINDINGS_REF from reference_versions.yaml and generates
6+
a requirements-reference.txt with pyprecice pinned to the corresponding version.
7+
Other common packages (numpy, matplotlib, setuptools) use fixed versions known
8+
to work with the tutorials.
9+
10+
Run from tools/tests/:
11+
python3 update_requirements_reference.py
12+
13+
Or to regenerate from a pip freeze (e.g. from Docker):
14+
pip freeze | python3 update_requirements_reference.py --from-freeze
15+
"""
16+
import argparse
17+
import re
18+
import sys
19+
from pathlib import Path
20+
21+
REFERENCE_VERSIONS = Path(__file__).parent / "reference_versions.yaml"
22+
REQUIREMENTS_REF = Path(__file__).parent / "requirements-reference.txt"
23+
24+
# Default pinned versions for common packages (fallback when not using --from-freeze)
25+
DEFAULTS = {
26+
"matplotlib": "3.9.0",
27+
"numpy": "1.26.4",
28+
"nutils": "7.2",
29+
"pyprecice": None, # From reference_versions.yaml
30+
"setuptools": ">=69.0.0",
31+
}
32+
33+
# Packages to include (in order)
34+
PACKAGES = ["matplotlib", "numpy", "nutils", "pyprecice", "setuptools"]
35+
36+
37+
def get_pyprecice_version_from_ref(ref: str) -> str:
38+
"""Convert PYTHON_BINDINGS_REF (e.g. v3.2.0) to pyprecice version (3.2.0)."""
39+
return ref.lstrip("v").strip()
40+
41+
42+
def load_reference_versions() -> str:
43+
"""Load PYTHON_BINDINGS_REF from reference_versions.yaml."""
44+
text = REFERENCE_VERSIONS.read_text()
45+
for line in text.splitlines():
46+
if "PYTHON_BINDINGS_REF" in line and ":" in line:
47+
match = re.search(r'["\']([^"\']+)["\']', line)
48+
if match:
49+
return match.group(1)
50+
raise ValueError("PYTHON_BINDINGS_REF not found in reference_versions.yaml")
51+
52+
53+
def parse_freezed_packages(freezed: str) -> dict[str, str]:
54+
"""Parse pip freeze output into {package: version}."""
55+
result = {}
56+
for line in freezed.strip().splitlines():
57+
line = line.strip()
58+
if not line or line.startswith("#"):
59+
continue
60+
if "==" in line:
61+
pkg, ver = line.split("==", 1)
62+
result[pkg.lower()] = f"=={ver.strip()}"
63+
elif "===" in line:
64+
pkg, ver = line.split("===", 1)
65+
result[pkg.lower()] = f"=={ver.strip()}"
66+
return result
67+
68+
69+
def main() -> None:
70+
parser = argparse.ArgumentParser(
71+
description="Update requirements-reference.txt from reference_versions.yaml"
72+
)
73+
parser.add_argument(
74+
"--from-freeze",
75+
action="store_true",
76+
help="Read pip freeze from stdin and use those versions for known packages",
77+
)
78+
args = parser.parse_args()
79+
80+
pyprecice_ref = load_reference_versions()
81+
pyprecice_ver = get_pyprecice_version_from_ref(pyprecice_ref)
82+
83+
if args.from_freeze:
84+
freezed = parse_freezed_packages(sys.stdin.read())
85+
versions = {}
86+
for pkg in PACKAGES:
87+
if pkg.lower() in freezed:
88+
versions[pkg] = freezed[pkg.lower()]
89+
elif pkg == "pyprecice":
90+
versions[pkg] = f"=={pyprecice_ver}"
91+
elif DEFAULTS.get(pkg):
92+
versions[pkg] = (
93+
DEFAULTS[pkg] if DEFAULTS[pkg].startswith(("==", ">=", "~=")) else f"=={DEFAULTS[pkg]}"
94+
)
95+
else:
96+
DEFAULTS["pyprecice"] = pyprecice_ver
97+
versions = {
98+
pkg: f"=={ver}" if ver and not ver.startswith(("==", ">=", "~=")) else (ver or "")
99+
for pkg, ver in DEFAULTS.items()
100+
}
101+
versions["pyprecice"] = f"=={pyprecice_ver}"
102+
103+
header = """# Pinned Python dependency versions for reproducible system tests and distributions.
104+
# Generated from reference_versions.yaml (PYTHON_BINDINGS_REF). Update at each release.
105+
# Run: python3 update_requirements_reference.py
106+
#
107+
# See tools/tests/README.md for how to update this file.
108+
109+
"""
110+
lines = [f"{pkg}{versions.get(pkg, '')}\n" for pkg in PACKAGES if pkg in versions]
111+
REQUIREMENTS_REF.write_text(header + "".join(lines))
112+
print(f"Wrote {REQUIREMENTS_REF} (pyprecice from {pyprecice_ref})")
113+
114+
115+
if __name__ == "__main__":
116+
main()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Validate that requirements-reference.txt exists and pyprecice version
4+
matches PYTHON_BINDINGS_REF in reference_versions.yaml.
5+
6+
Exit 0 on success, 1 on failure.
7+
"""
8+
import re
9+
import sys
10+
from pathlib import Path
11+
12+
TOOLS_TESTS = Path(__file__).parent
13+
REFERENCE_VERSIONS = TOOLS_TESTS / "reference_versions.yaml"
14+
REQUIREMENTS_REF = TOOLS_TESTS / "requirements-reference.txt"
15+
16+
17+
def main() -> int:
18+
if not REQUIREMENTS_REF.exists():
19+
print(f"ERROR: {REQUIREMENTS_REF} not found. Run update_requirements_reference.py.", file=sys.stderr)
20+
return 1
21+
22+
# Load PYTHON_BINDINGS_REF
23+
ref_text = REFERENCE_VERSIONS.read_text()
24+
ref_match = re.search(r'PYTHON_BINDINGS_REF:\s*["\']([^"\']+)["\']', ref_text)
25+
if not ref_match:
26+
print("ERROR: PYTHON_BINDINGS_REF not found in reference_versions.yaml", file=sys.stderr)
27+
return 1
28+
29+
expected_ver = ref_match.group(1).lstrip("v").strip()
30+
31+
# Parse pyprecice from requirements-reference.txt
32+
req_text = REQUIREMENTS_REF.read_text()
33+
precice_match = re.search(r"pyprecice\s*==\s*([\w.]+)", req_text)
34+
if not precice_match:
35+
print("ERROR: pyprecice not found in requirements-reference.txt", file=sys.stderr)
36+
return 1
37+
38+
actual_ver = precice_match.group(1).strip()
39+
if actual_ver != expected_ver:
40+
print(
41+
f"ERROR: pyprecice version mismatch: requirements-reference.txt has {actual_ver}, "
42+
f"reference_versions.yaml PYTHON_BINDINGS_REF has {ref_match.group(1)}. "
43+
"Run: python3 update_requirements_reference.py",
44+
file=sys.stderr,
45+
)
46+
return 1
47+
48+
print(f"OK: requirements-reference.txt pyprecice=={actual_ver} matches reference_versions.yaml")
49+
return 0
50+
51+
52+
if __name__ == "__main__":
53+
sys.exit(main())

0 commit comments

Comments
 (0)