| #!/usr/bin/env python |
| |
| import json |
| import subprocess |
| import sys |
| import tempfile |
| from pathlib import Path |
| |
| # Updates py/requirements.txt with latest compatible versions using pip |
| # Run with: bazel run //scripts:update_py_deps |
| |
| |
| def main(): |
| script_dir = Path(__file__).resolve().parent |
| requirements_file = script_dir.parent / "py" / "requirements.txt" |
| |
| if not requirements_file.exists(): |
| raise FileNotFoundError(f"{requirements_file} not found") |
| |
| print(f"Checking {requirements_file}") |
| |
| # Parse current requirements preserving original format |
| current_lines = requirements_file.read_text().strip().split("\n") |
| packages = [] # (original_line, package_name_with_extras, package_name_normalized) |
| for line in current_lines: |
| line = line.strip() |
| if line and not line.startswith("#") and "==" in line: |
| name_with_extras, version = line.split("==", 1) |
| # Normalize: remove extras for pip queries |
| name_normalized = name_with_extras.split("[")[0].lower() |
| packages.append((line, name_with_extras, name_normalized)) |
| |
| with tempfile.TemporaryDirectory() as tmpdir: |
| venv_dir = Path(tmpdir) / "venv" |
| |
| # Create virtual environment |
| print("Creating temporary virtual environment...") |
| subprocess.run( |
| [sys.executable, "-m", "venv", str(venv_dir)], |
| check=True, |
| capture_output=True, |
| ) |
| |
| pip = venv_dir / "bin" / "pip" |
| |
| # Upgrade pip first |
| subprocess.run( |
| [str(pip), "install", "-q", "--upgrade", "pip"], |
| check=True, |
| capture_output=True, |
| ) |
| |
| # Install packages (with extras) to let pip resolve versions |
| install_names = [p[1] for p in packages] # name_with_extras |
| print(f"Installing {len(install_names)} packages...") |
| result = subprocess.run( |
| [str(pip), "install", "-q"] + install_names, |
| capture_output=True, |
| text=True, |
| ) |
| if result.returncode != 0: |
| raise RuntimeError(f"Error installing packages:\n{result.stderr}") |
| |
| # Get installed versions |
| result = subprocess.run( |
| [str(pip), "list", "--format=json"], |
| capture_output=True, |
| text=True, |
| check=True, |
| ) |
| installed = {pkg["name"].lower(): pkg["version"] for pkg in json.loads(result.stdout)} |
| |
| # Update versions in original lines |
| updated_lines = [] |
| updates = [] |
| for orig_line, name_with_extras, name_normalized in packages: |
| old_version = orig_line.split("==")[1] |
| new_version = installed.get(name_normalized) |
| |
| if new_version and new_version != old_version: |
| updates.append((name_with_extras, old_version, new_version)) |
| updated_lines.append(f"{name_with_extras}=={new_version}") |
| print(f" {name_with_extras}: {old_version} -> {new_version}") |
| else: |
| updated_lines.append(orig_line) |
| |
| if not updates: |
| print("\nAll packages are up to date!") |
| return |
| |
| # Rebuild file preserving non-package lines |
| new_content = [] |
| pkg_idx = 0 |
| for line in current_lines: |
| stripped = line.strip() |
| if stripped and not stripped.startswith("#") and "==" in stripped: |
| new_content.append(updated_lines[pkg_idx]) |
| pkg_idx += 1 |
| else: |
| new_content.append(line) |
| |
| requirements_file.write_text("\n".join(new_content) + "\n") |
| print(f"\nUpdated {len(updates)} package(s)") |
| print("\nNow run: bazel run //py:requirements.update") |
| |
| |
| if __name__ == "__main__": |
| main() |