| |
| """ |
| multi_file_editor.py — ATYPIK v24.0 |
| Edition multi-fichiers avec patches unidiff et edits semantiques. |
| """ |
|
|
| import re |
| import ast |
| from pathlib import Path |
| from typing import Tuple, List, Dict |
| from dataclasses import dataclass |
|
|
| @dataclass |
| class EditResult: |
| success: bool |
| file: str |
| error: str = "" |
| original: str = "" |
| modified: str = "" |
|
|
| class MultiFileEditor: |
| def __init__(self, repo_path: Path): |
| self.repo_path = Path(repo_path) |
| self.edits: List[EditResult] = [] |
|
|
| def read_file(self, filepath: str) -> str: |
| fpath = self.repo_path / filepath |
| if not fpath.exists(): |
| return "" |
| return fpath.read_text(encoding="utf-8") |
|
|
| def write_file(self, filepath: str, content: str) -> bool: |
| try: |
| fpath = self.repo_path / filepath |
| fpath.parent.mkdir(parents=True, exist_ok=True) |
| fpath.write_text(content, encoding="utf-8") |
| return True |
| except Exception: |
| return False |
|
|
| def apply_edit(self, filepath: str, old_string: str, new_string: str) -> EditResult: |
| content = self.read_file(filepath) |
| if old_string not in content: |
| normalized_old = re.sub(r'\s+', ' ', old_string).strip() |
| normalized_content = re.sub(r'\s+', ' ', content) |
| if normalized_old not in normalized_content: |
| return EditResult(False, filepath, f"old_string not found in {filepath}") |
| new_content = content.replace(old_string, new_string, 1) |
| if filepath.endswith(".py"): |
| try: |
| ast.parse(new_content) |
| except SyntaxError as e: |
| return EditResult(False, filepath, f"Syntax error: {e}") |
| if self.write_file(filepath, new_content): |
| result = EditResult(True, filepath, "", content, new_content) |
| self.edits.append(result) |
| return result |
| return EditResult(False, filepath, "Write failed") |
|
|
| def apply_patch_unidiff(self, patch_content: str) -> List[EditResult]: |
| results = [] |
| lines = patch_content.split("\n") |
| current_file = None |
| file_lines = [] |
| hunk_old_start = 0 |
| i = 0 |
| while i < len(lines): |
| line = lines[i] |
| if line.startswith("diff --git"): |
| if current_file and file_lines: |
| result = self._apply_hunk(current_file, file_lines, hunk_old_start) |
| results.append(result) |
| match = re.search(r'b/(.+)$', line) |
| current_file = match.group(1) if match else None |
| file_lines = [] |
| i += 1 |
| continue |
| if line.startswith("@@"): |
| match = re.match(r'@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@', line) |
| if match: |
| hunk_old_start = int(match.group(1)) |
| i += 1 |
| continue |
| if current_file and (line.startswith('+') or line.startswith('-') or line.startswith(' ') or line == ''): |
| file_lines.append(line) |
| i += 1 |
| if current_file and file_lines: |
| result = self._apply_hunk(current_file, file_lines, hunk_old_start) |
| results.append(result) |
| return results |
|
|
| def _apply_hunk(self, filepath: str, hunk_lines: List[str], old_start: int) -> EditResult: |
| content = self.read_file(filepath) |
| if not content: |
| return EditResult(False, filepath, "File not found") |
| lines = content.split("\n") |
| new_lines = [] |
| hunk_idx = 0 |
| for i, line in enumerate(lines, 1): |
| if i < old_start: |
| new_lines.append(line) |
| continue |
| if hunk_idx >= len(hunk_lines): |
| new_lines.append(line) |
| continue |
| hunk_line = hunk_lines[hunk_idx] |
| if hunk_line.startswith('+'): |
| new_lines.append(hunk_line[1:]) |
| hunk_idx += 1 |
| elif hunk_line.startswith('-'): |
| if line.strip() == hunk_line[1:].strip(): |
| hunk_idx += 1 |
| else: |
| new_lines.append(line) |
| elif hunk_line.startswith(' '): |
| new_lines.append(hunk_line[1:]) |
| hunk_idx += 1 |
| else: |
| new_lines.append(line) |
| while hunk_idx < len(hunk_lines): |
| hl = hunk_lines[hunk_idx] |
| if hl.startswith('+'): |
| new_lines.append(hl[1:]) |
| hunk_idx += 1 |
| new_content = "\n".join(new_lines) |
| if filepath.endswith(".py"): |
| try: |
| ast.parse(new_content) |
| except SyntaxError as e: |
| return EditResult(False, filepath, f"Syntax error: {e}") |
| if self.write_file(filepath, new_content): |
| return EditResult(True, filepath) |
| return EditResult(False, filepath, "Write failed") |
|
|
| def get_summary(self) -> Dict: |
| return { |
| "total": len(self.edits), |
| "successful": sum(1 for e in self.edits if e.success), |
| "failed": sum(1 for e in self.edits if not e.success), |
| "files_modified": list(set(e.file for e in self.edits if e.success)) |
| } |
|
|