Files
i18n-guardian/i18n_guardian/diff/diff_generator.py
7000pctAUTO 8bacceea68
Some checks failed
CI / test (3.11) (push) Has been cancelled
CI / test (3.10) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled
Add diff and utils modules
2026-02-02 17:22:46 +00:00

169 lines
4.6 KiB
Python

"""Diff generation for i18n fixes."""
import difflib
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional
from rich.console import Console
from rich.syntax import Syntax
@dataclass
class DiffChange:
"""Represents a single change in a diff."""
file_path: Path
original_line: int
original_text: str
new_text: str
new_line: Optional[int] = None
@dataclass
class DiffResult:
"""Result of a diff operation."""
changes: List[DiffChange] = field(default_factory=list)
files_modified: int = 0
total_changes: int = 0
class DiffGenerator:
"""Generate diffs for i18n fixes."""
def __init__(self, color: bool = True) -> None:
self.color = color
self.console = Console()
def generate_diff(
self,
file_path: Path,
old_content: str,
new_content: str,
) -> str:
"""Generate a unified diff between old and new content."""
diff = difflib.unified_diff(
old_content.splitlines(keepends=True),
new_content.splitlines(keepends=True),
fromfile=str(file_path),
tofile=str(file_path),
n=3,
)
return "".join(diff)
def preview_diff(self, diff: str) -> None:
"""Preview a diff using Rich."""
if self.color:
self.console.print(Syntax(diff, "diff", line_numbers=True))
else:
self.console.print(diff)
def apply_diff(
self,
file_path: Path,
diff: str,
backup: bool = True,
) -> bool:
"""Apply a diff to a file."""
try:
if backup:
backup_path = file_path.with_suffix(".bak")
backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8")
lines = file_path.read_text(encoding="utf-8").splitlines(keepends=True)
diff_lines = diff.splitlines(keepends=True)
patched_lines = self._apply_diff_lines(lines, diff_lines)
new_content = "".join(patched_lines)
file_path.write_text(new_content, encoding="utf-8")
return True
except (OSError, UnicodeDecodeError) as e:
print(f"Error applying diff: {e}")
return False
def _apply_diff_lines(
self,
lines: List[str],
diff_lines: List[str],
) -> List[str]:
"""Apply diff lines to document."""
result = []
i = 0
j = 0
while j < len(diff_lines):
diff_line = diff_lines[j]
if diff_line.startswith("---") or diff_line.startswith("+++"):
j += 1
continue
if diff_line.startswith("@@"):
j += 1
continue
if diff_line.startswith("-"):
original_line = diff_line[1:]
added_content = ""
if "+" in original_line:
parts = original_line.split("+", 1)
original_line = parts[0]
added_content = parts[1] if len(parts) > 1 else ""
while i < len(lines) and lines[i].rstrip("\n") != original_line.rstrip("\n"):
result.append(lines[i])
i += 1
if i < len(lines):
i += 1
if added_content:
result.append(added_content)
j += 1
continue
if diff_line.startswith("+"):
result.append(diff_line[1:])
j += 1
continue
if diff_line.startswith(" "):
result.append(lines[i] if i < len(lines) else diff_line[1:])
i += 1
j += 1
continue
j += 1
while i < len(lines):
result.append(lines[i])
i += 1
return result
def create_replacement(
self,
original_text: str,
replacement: str,
i18n_function: str = "t",
) -> str:
"""Create a replacement string with i18n function call."""
escaped = original_text.replace("\\", "\\\\").replace('"', '\\"')
return f'{i18n_function}("{escaped}")'
def preview_change(
self,
file_path: Path,
original_text: str,
replacement: str,
line_number: int,
) -> None:
"""Preview a single change."""
diff = self.generate_diff(
file_path,
original_text,
original_text.replace(original_text, replacement, 1),
)
self.preview_diff(diff)