#!/usr/bin/env python3 """ 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)) }