DeepCritical / .pre-commit-hooks /run_pytest_with_sync.py
Joseph Pollack
adds or improves : interface , tests, docs , ci , precommit , build , and demo
ce644a9 unverified
raw
history blame
6.48 kB
#!/usr/bin/env python3
"""Cross-platform pytest runner that syncs dependencies before running tests."""
import shutil
import subprocess
import sys
from pathlib import Path
def clean_caches(project_root: Path) -> None:
"""Remove pytest and Python cache directories and files.
Only scans specific directories (src/, tests/) to avoid resource
exhaustion from scanning large directories like .venv on Windows.
"""
# Directories to scan for caches (only project code, not dependencies)
scan_dirs = ["src", "tests", ".pre-commit-hooks"]
# Directories to exclude (to avoid resource issues)
exclude_dirs = {
".venv",
"venv",
"ENV",
"env",
".git",
"node_modules",
"dist",
"build",
".eggs",
"reference_repos",
"folder",
}
cache_patterns = [
".pytest_cache",
"__pycache__",
"*.pyc",
"*.pyo",
"*.pyd",
".mypy_cache",
".ruff_cache",
]
def should_exclude(path: Path) -> bool:
"""Check if a path should be excluded from cache cleanup."""
# Check if any parent directory is in exclude list
for parent in path.parents:
if parent.name in exclude_dirs:
return True
# Check if the path itself is excluded
if path.name in exclude_dirs:
return True
return False
cleaned = []
# Only scan specific directories to avoid resource exhaustion
for scan_dir in scan_dirs:
scan_path = project_root / scan_dir
if not scan_path.exists():
continue
for pattern in cache_patterns:
if "*" in pattern:
# Handle glob patterns for files
try:
for cache_file in scan_path.rglob(pattern):
if should_exclude(cache_file):
continue
try:
if cache_file.is_file():
cache_file.unlink()
cleaned.append(str(cache_file.relative_to(project_root)))
except OSError:
pass # Ignore errors (file might be locked or already deleted)
except OSError:
pass # Ignore errors during directory traversal
else:
# Handle directory patterns
try:
for cache_dir in scan_path.rglob(pattern):
if should_exclude(cache_dir):
continue
try:
if cache_dir.is_dir():
shutil.rmtree(cache_dir, ignore_errors=True)
cleaned.append(str(cache_dir.relative_to(project_root)))
except OSError:
pass # Ignore errors (directory might be locked)
except OSError:
pass # Ignore errors during directory traversal
# Also clean root-level caches (like .pytest_cache in project root)
for pattern in [".pytest_cache", ".mypy_cache", ".ruff_cache"]:
cache_path = project_root / pattern
if cache_path.exists() and cache_path.is_dir():
try:
shutil.rmtree(cache_path, ignore_errors=True)
cleaned.append(pattern)
except OSError:
pass
if cleaned:
print(f"Cleaned {len(cleaned)} cache items")
else:
print("No cache files found to clean")
def run_command(
cmd: list[str], check: bool = True, shell: bool = False, cwd: str | None = None
) -> int:
"""Run a command and return exit code."""
try:
result = subprocess.run(
cmd,
check=check,
shell=shell,
cwd=cwd,
env=None, # Use current environment, uv will handle venv
)
return result.returncode
except subprocess.CalledProcessError as e:
return e.returncode
except FileNotFoundError:
print(f"Error: Command not found: {cmd[0]}")
return 1
def main() -> int:
"""Main entry point."""
import os
# Get the project root (where pyproject.toml is)
script_dir = Path(__file__).parent
project_root = script_dir.parent
# Change to project root to ensure uv works correctly
os.chdir(project_root)
# Clean caches before running tests
print("Cleaning pytest and Python caches...")
clean_caches(project_root)
# Check if uv is available
if run_command(["uv", "--version"], check=False) != 0:
print("Error: uv not found. Please install uv: https://github.com/astral-sh/uv")
return 1
# Parse arguments
test_type = sys.argv[1] if len(sys.argv) > 1 else "unit"
extra_args = sys.argv[2:] if len(sys.argv) > 2 else []
# Sync dependencies - always include dev
# Note: embeddings dependencies are now in main dependencies, not optional
# Use --extra dev for [project.optional-dependencies].dev (not --dev which is for [dependency-groups])
sync_cmd = ["uv", "sync", "--extra", "dev"]
print(f"Syncing dependencies for {test_type} tests...")
if run_command(sync_cmd, cwd=project_root) != 0:
return 1
# Build pytest command - use uv run to ensure correct environment
if test_type == "unit":
pytest_args = [
"tests/unit/",
"-v",
"-m",
"not openai and not embedding_provider",
"--tb=short",
"-p",
"no:logfire",
"--cache-clear", # Clear pytest cache before running
]
elif test_type == "embeddings":
pytest_args = [
"tests/",
"-v",
"-m",
"local_embeddings",
"--tb=short",
"-p",
"no:logfire",
"--cache-clear", # Clear pytest cache before running
]
else:
pytest_args = []
pytest_args.extend(extra_args)
# Use uv run python -m pytest to ensure we use the venv's pytest
# This is more reliable than uv run pytest which might find system pytest
pytest_cmd = ["uv", "run", "python", "-m", "pytest", *pytest_args]
print(f"Running {test_type} tests...")
return run_command(pytest_cmd, cwd=project_root)
if __name__ == "__main__":
sys.exit(main())