Source code for validate_actions.globals.fixer
"""Fixer module for applying changes to YAML workflow files."""
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Dict, List
from validate_actions.globals.problems import Problem, ProblemLevel
[docs]
class Fixer(ABC):
"""Abstract base for applying fixes to YAML workflow files."""
[docs]
@abstractmethod
def edit_yaml_at_position(
self, idx: int, old_text: str, new_text: str, problem: Problem, new_problem_desc: str
) -> Problem:
"""Queue an edit to replace text at a specific character position.
Args:
idx: Character index where replacement starts
old_text: Text to be replaced (for validation)
new_text: Replacement text
problem: Problem instance to update
new_problem_desc: New description for the fixed problem
Returns:
Updated problem instance
"""
pass
[docs]
@abstractmethod
def flush(self) -> None:
"""Apply all queued edits to the file."""
pass
[docs]
class BaseFixer(Fixer):
"""Default fixer that batches edits and applies them on flush."""
file_path: Path
pending_edits: List[Dict[str, Any]]
def __init__(self, file_path: Path):
self.file_path = file_path
self.pending_edits = []
[docs]
def edit_yaml_at_position(
self, idx: int, old_text: str, new_text: str, problem: Problem, new_problem_desc: str
) -> Problem:
"""Queue an edit for later application and mark problem as fixed.
Args:
idx: Character index where replacement starts
old_text: Text to be replaced (for validation)
new_text: Replacement text
problem: Problem instance to update
new_problem_desc: New description for the fixed problem
Returns:
Updated problem instance with NON level
"""
# Batch the edit instead of applying immediately
edit = {
"idx": idx,
"num_delete": len(old_text),
"new_text": new_text,
"problem": problem,
"new_problem_desc": new_problem_desc,
}
self.pending_edits.append(edit)
# Update problem to reflect that it will be fixed
problem.level = ProblemLevel.NON
problem.desc = new_problem_desc
return problem
[docs]
def flush(self) -> None:
"""Apply all pending edits to the file in descending position order."""
if not self.pending_edits:
return
try:
# Read current file content
with open(self.file_path, "r", encoding="utf-8") as f:
content = f.read()
# Sort edits by position in descending order (end-of-file to beginning)
# This ensures later edits don't affect the positions of earlier edits
sorted_edits = sorted(self.pending_edits, key=lambda edit: edit["idx"], reverse=True)
# Apply edits in descending position order
for edit in sorted_edits:
idx = edit["idx"]
num_delete = edit["num_delete"]
new_text = edit["new_text"]
# Validate position bounds
if idx < 0 or idx > len(content):
continue
# Apply edit: delete and insert
content = content[:idx] + new_text + content[idx + num_delete :]
# Write updated content back to file
with open(self.file_path, "w", encoding="utf-8") as f:
f.write(content)
# Clear pending edits after successful application
self.pending_edits.clear()
except (OSError, UnicodeError):
# On error, leave pending_edits intact for potential retry
pass
[docs]
class NoFixer(Fixer):
"""A fixer that does nothing. Used when no fixes are needed."""
[docs]
def edit_yaml_at_position(
self, idx: int, old_text: str, new_text: str, problem: Problem, new_problem_desc: str
) -> Problem:
"""No-op implementation that returns the problem unchanged.
Args:
idx: Character index (ignored)
old_text: Text to be replaced (ignored)
new_text: Replacement text (ignored)
problem: Problem instance to return
new_problem_desc: New description (ignored)
Returns:
Unmodified problem instance
"""
# No-op, just return the problem as is
return problem
[docs]
def flush(self) -> None:
"""No-op implementation with no effects."""
# No-op, nothing to flush
pass