chore: import upstream snapshot with attribution
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Apply Type Hints from suggestions JSON to source files.
|
||||
|
||||
Reads /tmp/type-hints-suggestions.json (generated by generate-type-hints.py),
|
||||
applies typed signatures to source files using AST-based function lookup.
|
||||
|
||||
Safety: validates each file with py_compile after modification; reverts on failure.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import json
|
||||
import py_compile
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROTECTED_PATTERNS = [
|
||||
".github/workflows/",
|
||||
".env",
|
||||
"ods/installers/",
|
||||
"ods/ods-cli",
|
||||
"ods/config/",
|
||||
]
|
||||
|
||||
|
||||
def is_protected(file_path: str) -> bool:
|
||||
"""Check if a file path matches any protected patterns."""
|
||||
for pattern in PROTECTED_PATTERNS:
|
||||
if file_path.startswith(pattern) or f"/{pattern}" in file_path:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def find_function_line(source: str, function_name: str) -> int | None:
|
||||
"""Use AST to find the actual line number of a function by name."""
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
return None
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
if node.name == function_name:
|
||||
return node.lineno
|
||||
return None
|
||||
|
||||
|
||||
def find_def_end_line(lines: list[str], start_idx: int) -> int:
|
||||
"""Find the line index where a def statement ends (the line with the colon)."""
|
||||
depth = 0
|
||||
for i in range(start_idx, len(lines)):
|
||||
line = lines[i]
|
||||
depth += line.count("(") - line.count(")")
|
||||
if depth <= 0 and ":" in line:
|
||||
stripped = line.rstrip()
|
||||
if stripped.endswith(":"):
|
||||
return i
|
||||
colon_pos = stripped.rfind(":")
|
||||
if colon_pos >= 0:
|
||||
return i
|
||||
return start_idx
|
||||
|
||||
|
||||
def get_existing_imports(source: str) -> set[str]:
|
||||
"""Extract all existing import statements from source."""
|
||||
imports = set()
|
||||
for line in source.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("import ") or stripped.startswith("from "):
|
||||
imports.add(stripped)
|
||||
return imports
|
||||
|
||||
|
||||
def find_last_import_line(lines: list[str]) -> int:
|
||||
"""Find the 0-based index of the last import line in the file."""
|
||||
last_import = -1
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("import ") or stripped.startswith("from "):
|
||||
last_import = i
|
||||
return last_import
|
||||
|
||||
|
||||
def normalize_import(imp: str) -> list[str]:
|
||||
"""Normalize an import statement."""
|
||||
return [imp.strip()]
|
||||
|
||||
|
||||
def apply_type_hints(suggestions_path: str) -> dict:
|
||||
"""Apply generated type hint suggestions to Python function signatures."""
|
||||
with open(suggestions_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
functions = data.get("functions_annotated", [])
|
||||
if not functions:
|
||||
print("No type hint suggestions to apply.")
|
||||
return {"files_modified": 0, "functions_applied": 0, "files_reverted": 0}
|
||||
|
||||
by_file: dict[str, list] = {}
|
||||
for func in functions:
|
||||
file_path = func.get("file", "")
|
||||
if not file_path:
|
||||
continue
|
||||
if is_protected(file_path):
|
||||
print(f" Skipping protected file: {file_path}")
|
||||
continue
|
||||
if not Path(file_path).exists():
|
||||
print(f" Skipping missing file: {file_path}")
|
||||
continue
|
||||
by_file.setdefault(file_path, []).append(func)
|
||||
|
||||
files_modified = 0
|
||||
functions_applied = 0
|
||||
files_reverted = 0
|
||||
|
||||
for file_path, file_funcs in by_file.items():
|
||||
print(f"\nProcessing {file_path} ({len(file_funcs)} functions)...")
|
||||
|
||||
original_content = Path(file_path).read_text()
|
||||
source = original_content
|
||||
lines = source.splitlines(keepends=True)
|
||||
|
||||
located = []
|
||||
for func in file_funcs:
|
||||
actual_line = find_function_line(source, func["function"])
|
||||
if actual_line is None:
|
||||
print(
|
||||
f" Could not find function '{func['function']}' in AST, skipping"
|
||||
)
|
||||
continue
|
||||
located.append((actual_line, func))
|
||||
|
||||
located.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
applied_in_file = 0
|
||||
for actual_line, func in located:
|
||||
typed_sig = func.get("typed_signature", "").strip()
|
||||
if not typed_sig:
|
||||
continue
|
||||
|
||||
start_idx = actual_line - 1
|
||||
if start_idx >= len(lines) or start_idx < 0:
|
||||
continue
|
||||
|
||||
end_idx = find_def_end_line(lines, start_idx)
|
||||
|
||||
current_line = lines[start_idx]
|
||||
indent = current_line[: len(current_line) - len(current_line.lstrip())]
|
||||
|
||||
current_stripped = current_line.lstrip()
|
||||
is_async = current_stripped.startswith("async ")
|
||||
|
||||
typed_stripped = typed_sig.lstrip()
|
||||
if is_async and not typed_stripped.startswith("async "):
|
||||
typed_sig = "async " + typed_sig
|
||||
elif not is_async and typed_stripped.startswith("async "):
|
||||
typed_sig = typed_sig.replace("async ", "", 1)
|
||||
|
||||
if not typed_sig.rstrip().endswith(":"):
|
||||
typed_sig = typed_sig.rstrip() + ":"
|
||||
|
||||
new_line = indent + typed_sig.strip() + "\n"
|
||||
|
||||
lines[start_idx : end_idx + 1] = [new_line]
|
||||
applied_in_file += 1
|
||||
print(f" Applied: {func['function']} (line {actual_line})")
|
||||
|
||||
if applied_in_file == 0:
|
||||
continue
|
||||
|
||||
new_source = "".join(lines)
|
||||
existing_imports = get_existing_imports(new_source)
|
||||
|
||||
imports_to_add = []
|
||||
for func in file_funcs:
|
||||
for imp in func.get("imports_needed", []):
|
||||
for normalized in normalize_import(imp):
|
||||
if normalized not in existing_imports:
|
||||
imports_to_add.append(normalized)
|
||||
existing_imports.add(normalized)
|
||||
|
||||
if imports_to_add:
|
||||
lines = new_source.splitlines(keepends=True)
|
||||
insert_idx = find_last_import_line(lines)
|
||||
if insert_idx >= 0:
|
||||
insert_point = insert_idx + 1
|
||||
else:
|
||||
insert_point = 0
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if (
|
||||
stripped.startswith("#")
|
||||
or stripped.startswith('"""')
|
||||
or stripped.startswith("'''")
|
||||
or not stripped
|
||||
):
|
||||
insert_point = i + 1
|
||||
else:
|
||||
break
|
||||
|
||||
import_lines = [imp + "\n" for imp in imports_to_add]
|
||||
lines[insert_point:insert_point] = import_lines
|
||||
new_source = "".join(lines)
|
||||
|
||||
Path(file_path).write_text(new_source)
|
||||
|
||||
try:
|
||||
py_compile.compile(file_path, doraise=True)
|
||||
files_modified += 1
|
||||
functions_applied += applied_in_file
|
||||
print(f" Validated: {file_path} ({applied_in_file} functions)")
|
||||
except py_compile.PyCompileError as e:
|
||||
print(f" REVERT: {file_path} failed compilation: {e}")
|
||||
Path(file_path).write_text(original_content)
|
||||
files_reverted += 1
|
||||
|
||||
return {
|
||||
"files_modified": files_modified,
|
||||
"functions_applied": functions_applied,
|
||||
"files_reverted": files_reverted,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Load and apply generated type hints."""
|
||||
suggestions_path = (
|
||||
sys.argv[1] if len(sys.argv) > 1 else "/tmp/type-hints-suggestions.json"
|
||||
)
|
||||
|
||||
if not Path(suggestions_path).exists():
|
||||
print(f"Suggestions file not found: {suggestions_path}")
|
||||
print("No type hints to apply.")
|
||||
return
|
||||
|
||||
print(f"Loading suggestions from {suggestions_path}...")
|
||||
result = apply_type_hints(suggestions_path)
|
||||
|
||||
print("\n## Type Hints Application Summary\n")
|
||||
print(f"- **Files modified**: {result['files_modified']}")
|
||||
print(f"- **Functions updated**: {result['functions_applied']}")
|
||||
print(f"- **Files reverted** (compilation failed): {result['files_reverted']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user