Add diff and utils modules
This commit is contained in:
168
i18n_guardian/diff/diff_generator.py
Normal file
168
i18n_guardian/diff/diff_generator.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user