Skip to content

Commit 59186b9

Browse files
committed
Pin requirements script
Signed-off-by: Mihai Criveti <[email protected]>
1 parent c163a60 commit 59186b9

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

.github/tools/pin_requirements.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
"""Extract dependencies from pyproject.toml and pin versions.
3+
4+
Copyright 2025 Mihai Criveti
5+
SPDX-License-Identifier: Apache-2.0
6+
Authors: Mihai Criveti
7+
8+
This script reads the dependencies from pyproject.toml and converts
9+
version specifiers from >= to == for reproducible builds.
10+
"""
11+
12+
import tomllib
13+
import re
14+
import sys
15+
from pathlib import Path
16+
17+
18+
def pin_requirements(pyproject_path="pyproject.toml", output_path="requirements.txt"):
19+
"""
20+
Extract dependencies from pyproject.toml and pin versions.
21+
22+
Args:
23+
pyproject_path: Path to pyproject.toml file
24+
output_path: Path to output requirements.txt file
25+
"""
26+
try:
27+
with open(pyproject_path, "rb") as f:
28+
data = tomllib.load(f)
29+
except FileNotFoundError:
30+
print(f"Error: {pyproject_path} not found!", file=sys.stderr)
31+
sys.exit(1)
32+
except tomllib.TOMLDecodeError as e:
33+
print(f"Error parsing TOML: {e}", file=sys.stderr)
34+
sys.exit(1)
35+
36+
# Extract dependencies
37+
dependencies = data.get("project", {}).get("dependencies", [])
38+
if not dependencies:
39+
print("Warning: No dependencies found in pyproject.toml", file=sys.stderr)
40+
return
41+
42+
pinned_deps = []
43+
converted_count = 0
44+
45+
for dep in dependencies:
46+
# Match package name with optional extras and version
47+
# Pattern: package_name[optional_extras]>=version
48+
match = re.match(r'^([a-zA-Z0-9_-]+)(?:\[.*\])?>=(.+)', dep)
49+
50+
if match:
51+
name, version = match.groups()
52+
pinned_deps.append(f"{name}=={version}")
53+
converted_count += 1
54+
else:
55+
# Keep as-is if not in expected format
56+
pinned_deps.append(dep)
57+
print(f"Info: Keeping '{dep}' as-is (no >= pattern found)")
58+
59+
# Sort dependencies for consistency
60+
pinned_deps.sort(key=lambda x: x.lower())
61+
62+
# Write to requirements.txt
63+
with open(output_path, "w") as f:
64+
for dep in pinned_deps:
65+
f.write(f"{dep}\n")
66+
67+
print(f"✓ Generated {output_path} with {len(pinned_deps)} dependencies")
68+
print(f"✓ Converted {converted_count} dependencies from >= to ==")
69+
70+
# Show first few dependencies as preview
71+
if pinned_deps:
72+
print("\nPreview of pinned dependencies:")
73+
for dep in pinned_deps[:5]:
74+
print(f" - {dep}")
75+
if len(pinned_deps) > 5:
76+
print(f" ... and {len(pinned_deps) - 5} more")
77+
78+
79+
def main():
80+
"""Main entry point."""
81+
import argparse
82+
83+
parser = argparse.ArgumentParser(
84+
description="Extract and pin dependencies from pyproject.toml"
85+
)
86+
parser.add_argument(
87+
"-i", "--input",
88+
default="pyproject.toml",
89+
help="Path to pyproject.toml file (default: pyproject.toml)"
90+
)
91+
parser.add_argument(
92+
"-o", "--output",
93+
default="requirements.txt",
94+
help="Path to output requirements file (default: requirements.txt)"
95+
)
96+
parser.add_argument(
97+
"--dry-run",
98+
action="store_true",
99+
help="Print dependencies without writing to file"
100+
)
101+
102+
args = parser.parse_args()
103+
104+
if args.dry_run:
105+
# Dry run mode - just print what would be written
106+
try:
107+
with open(args.input, "rb") as f:
108+
data = tomllib.load(f)
109+
except FileNotFoundError:
110+
print(f"Error: {args.input} not found!", file=sys.stderr)
111+
sys.exit(1)
112+
113+
dependencies = data.get("project", {}).get("dependencies", [])
114+
print("Would generate the following pinned dependencies:\n")
115+
116+
for dep in sorted(dependencies, key=lambda x: x.lower()):
117+
match = re.match(r'^([a-zA-Z0-9_-]+)(?:\[.*\])?>=(.+)', dep)
118+
if match:
119+
name, version = match.groups()
120+
print(f"{name}=={version}")
121+
else:
122+
print(dep)
123+
else:
124+
pin_requirements(args.input, args.output)
125+
126+
127+
if __name__ == "__main__":
128+
main()

docs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
presentations/
12
.DS_Store
23
env
34
site/

0 commit comments

Comments
 (0)