Files
posthog/bin/check_uv_python_compatibility.py
2025-10-27 10:52:22 +01:00

232 lines
7.4 KiB
Python
Executable File

#!/usr/bin/env python3
# ruff: noqa: T201 allow print statements
"""
Validate uv configuration for local development and CI.
Performs two checks:
1. Verify flox's uv can download the Python version from pyproject.toml
(critical for local development)
2. Verify uv versions are consistent across flox and CI workflows
(helps maintain configuration consistency)
Run in CI via .github/workflows/ci-python.yml to catch issues early.
Exit codes:
0: All checks passed
1: Flox uv cannot download required Python or config error
"""
import re
import sys
import subprocess
from pathlib import Path
try:
import tomllib
except ImportError:
import tomli as tomllib # type: ignore
def get_python_version_from_pyproject() -> str:
"""Extract the required Python version from pyproject.toml."""
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)
requires_python = data.get("project", {}).get("requires-python", "")
# Extract version from format like "==3.12.11" or ">=3.12.11"
match = re.search(r"(\d+\.\d+\.\d+)", requires_python)
if not match:
raise ValueError(f"Could not parse Python version from: {requires_python}")
return match.group(1)
def get_uv_version_from_flox() -> str | None:
"""Extract uv version from flox manifest."""
flox_manifest = Path(__file__).parent.parent / ".flox" / "env" / "manifest.toml"
if not flox_manifest.exists():
return None
with open(flox_manifest, "rb") as f:
data = tomllib.load(f)
# Look for uv in the install section
install = data.get("install", {})
uv_config = install.get("uv", {})
return uv_config.get("version")
def get_uv_versions_from_workflows() -> dict[str, set[str]]:
"""Extract uv versions from GitHub workflow files.
Returns a dict mapping workflow filename to set of uv versions found.
Workflows may have multiple setup-uv steps with different versions.
"""
workflows_dir = Path(__file__).parent.parent / ".github" / "workflows"
uv_versions: dict[str, set[str]] = {}
for workflow_file in workflows_dir.glob("*.yml"):
content = workflow_file.read_text()
# Look for uv version specifications like "version: 0.8.19"
matches = re.findall(r"setup-uv@[a-f0-9]+.*?version:\s*([0-9.]+)", content, re.DOTALL)
if matches:
uv_versions[workflow_file.name] = set(matches)
return uv_versions
def check_uv_python_compatibility(uv_version: str, python_version: str) -> tuple[bool, str]:
"""
Check if a given uv version can download the specified Python version.
Uses `uvx --from uv@{version} uv python list --only-downloads` to test
the specific uv version's ability to download the Python version.
Returns:
tuple: (is_compatible, message)
"""
try:
# Use uvx to run the specific uv version
result = subprocess.run(
[
"uvx",
"--from",
f"uv@{uv_version}",
"uv",
"python",
"list",
python_version,
"--only-downloads",
],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0 and result.stdout.strip():
return True, "verified via uv python list"
else:
return False, f"Python {python_version} not available for uv {uv_version}"
except subprocess.TimeoutExpired:
return True, "uv command timed out, assuming compatible"
except FileNotFoundError:
return True, "uvx not installed, assuming compatible"
except Exception as e:
return True, f"unable to verify ({type(e).__name__})"
def main() -> int:
"""Main entry point for the validation script."""
print("Validating uv configuration...")
print()
# Get Python version from pyproject.toml
try:
python_version = get_python_version_from_pyproject()
print(f"Python version required: {python_version}")
except Exception as e:
print(f"✗ Error reading Python version: {e}")
return 1
# Get uv versions from all sources
flox_uv = None
try:
flox_uv = get_uv_version_from_flox()
if flox_uv:
print(f"Flox uv version: {flox_uv}")
except Exception as e:
print(f"⚠ Warning: Could not read flox manifest: {e}")
workflow_versions = {}
try:
workflow_versions = get_uv_versions_from_workflows()
if workflow_versions:
total_versions = sum(len(versions) for versions in workflow_versions.values())
print(f"Found {total_versions} uv installation(s) in {len(workflow_versions)} workflow(s)")
except Exception as e:
print(f"⚠ Warning: Could not read workflows: {e}")
print()
print("=" * 60)
print()
# Check 1: Can flox uv download the required Python version?
print("Check 1: Flox uv Python compatibility")
print("-" * 60)
if not flox_uv:
print("⚠ Skipped: No flox manifest found")
flox_compatible = True
else:
compatible, message = check_uv_python_compatibility(flox_uv, python_version)
if compatible:
print(f"✓ Flox uv {flox_uv} can download Python {python_version}")
print(f" {message}")
flox_compatible = True
else:
print(f"✗ Flox uv {flox_uv} cannot download Python {python_version}")
print(f" {message}")
print()
print(" To fix: Update uv version in .flox/env/manifest.toml")
print(" See: https://github.com/astral-sh/uv/releases")
flox_compatible = False
print()
print("=" * 60)
print()
# Check 2: Are all uv versions consistent?
print("Check 2: uv version consistency")
print("-" * 60)
# Flatten workflow versions for consistency check
all_versions = {}
if flox_uv:
all_versions["flox"] = flox_uv
for workflow_name, versions in workflow_versions.items():
if len(versions) == 1:
all_versions[workflow_name] = next(iter(versions))
else:
# Multiple versions in one workflow
for i, version in enumerate(sorted(versions), 1):
all_versions[f"{workflow_name}#{i}"] = version
if len(all_versions) < 2:
print("⚠ Not enough sources to check consistency")
versions_consistent = True
else:
unique_versions = set(all_versions.values())
if len(unique_versions) == 1:
print(f"✓ All sources use uv {unique_versions.pop()}")
versions_consistent = True
else:
print("⚠ Inconsistent uv versions found:")
for source, version in sorted(all_versions.items()):
print(f" - {source}: {version}")
print()
print(" Consider standardizing to a single uv version")
versions_consistent = False
print()
print("=" * 60)
print()
# Summary
if flox_compatible and versions_consistent:
print("✓ All checks passed")
return 0
elif not flox_compatible:
print("✗ Failed: Flox uv cannot download required Python version")
return 1
else:
print("⚠ Passed with warnings: Version inconsistency detected")
return 0
if __name__ == "__main__":
sys.exit(main())