Spaces:
Sleeping
Sleeping
| """Patch parsing and application utilities.""" | |
| from __future__ import annotations | |
| import json | |
| from dataclasses import dataclass | |
| from typing import List | |
| class LineReplacement: | |
| """Single line replacement patch operation.""" | |
| line_no: int | |
| content: str | |
| class PatchError(ValueError): | |
| """Raised when a patch payload is invalid.""" | |
| def parse_patch(patch: str) -> List[LineReplacement] | None: | |
| """ | |
| Parse a patch payload. | |
| Supported formats: | |
| - Full replacement source: any non-JSON string. Returns ``None``. | |
| - JSON array of objects: [{"line_no": 3, "content": "fixed line"}] | |
| """ | |
| try: | |
| data = json.loads(patch) | |
| except json.JSONDecodeError: | |
| return None | |
| if not isinstance(data, list): | |
| raise PatchError("Patch JSON must be an array of line replacement objects.") | |
| replacements: List[LineReplacement] = [] | |
| for item in data: | |
| if not isinstance(item, dict): | |
| raise PatchError("Each patch entry must be an object.") | |
| line_no = item.get("line_no") | |
| content = item.get("content") | |
| if not isinstance(line_no, int) or line_no < 1: | |
| raise PatchError("Patch line_no must be a positive integer.") | |
| if not isinstance(content, str): | |
| raise PatchError("Patch content must be a string.") | |
| replacements.append(LineReplacement(line_no=line_no, content=content)) | |
| return replacements | |
| def apply_patch(source: str, patch: str) -> tuple[str, int]: | |
| """Apply a patch and return the patched source plus changed-line count.""" | |
| replacements = parse_patch(patch) | |
| if replacements is None: | |
| normalized = patch.replace("\r\n", "\n") | |
| changed = _count_changed_lines(source, normalized) | |
| return normalized, changed | |
| lines = source.splitlines() | |
| changed = 0 | |
| for replacement in replacements: | |
| if replacement.line_no > len(lines): | |
| raise PatchError( | |
| f"Patch line {replacement.line_no} is outside the source file." | |
| ) | |
| idx = replacement.line_no - 1 | |
| if lines[idx] != replacement.content: | |
| changed += 1 | |
| lines[idx] = replacement.content | |
| patched = "\n".join(lines) | |
| if source.endswith("\n"): | |
| patched += "\n" | |
| return patched, changed | |
| def _count_changed_lines(original: str, patched: str) -> int: | |
| original_lines = original.splitlines() | |
| patched_lines = patched.splitlines() | |
| overlap = min(len(original_lines), len(patched_lines)) | |
| changed = sum( | |
| 1 for idx in range(overlap) if original_lines[idx] != patched_lines[idx] | |
| ) | |
| changed += abs(len(original_lines) - len(patched_lines)) | |
| return changed | |