Download src/yaml_edit/engine.py from erayack/yaml-edit: direct link, hf CLI and curl.
- Browser
- Download file 14.3 kB
-
https://huggingface.co/erayack/yaml-edit/resolve/main/src/yaml_edit/engine.py
- Command line
-
hf download hf://erayack/yaml-edit/src/yaml_edit/engine.py
-
curl -L -o engine.py https://huggingface.co/erayack/yaml-edit/resolve/main/src/yaml_edit/engine.py
14.3 kB
| """Allowlisted scalar edits with exact snapshot binding and post-edit verification.""" | |
| import json | |
| from collections.abc import Sequence | |
| from difflib import unified_diff | |
| from typing import Never, cast | |
| from ruamel.yaml import YAML | |
| from ruamel.yaml.nodes import ScalarNode | |
| from .models import ( | |
| MAX_BATCH_CANDIDATE_BYTES, | |
| MAX_BATCH_OPERATIONS, | |
| MAX_BATCH_SCALAR_BYTES, | |
| BatchEntry, | |
| BatchPreview, | |
| BatchProposal, | |
| BatchRequest, | |
| Candidate, | |
| EditPreview, | |
| EditProposal, | |
| ErrorCode, | |
| Inspection, | |
| Path, | |
| Scalar, | |
| Tree, | |
| YamlEditError, | |
| is_scalar, | |
| validate_batch_proposal, | |
| ) | |
| from .parsing import Parsed, parse_source | |
| from .validation import validate_value | |
| def inspect(source: str) -> Inspection: | |
| """Discover editable fields and bind their identities to this exact source.""" | |
| parsed = parse_source(source) | |
| return Inspection(parsed.source_version, parsed.candidates) | |
| def _candidate(parsed: Parsed, candidate_id: str) -> Candidate: | |
| for candidate in parsed.candidates: | |
| if candidate.id == candidate_id: | |
| return candidate | |
| raise YamlEditError( | |
| ErrorCode.UNKNOWN_TARGET, f"Not an editable existing field: {candidate_id!r}." | |
| ) | |
| def _same(left: Tree, right: Tree) -> bool: | |
| """Compare semantic trees without conflating bool/int or int/float.""" | |
| if type(left) is not type(right): | |
| return False | |
| if isinstance(left, dict) and isinstance(right, dict): | |
| return left.keys() == right.keys() and all(_same(left[key], right[key]) for key in left) | |
| if isinstance(left, list) and isinstance(right, list): | |
| return len(left) == len(right) and all( | |
| _same(a, b) for a, b in zip(left, right, strict=True) | |
| ) | |
| return left == right | |
| def _with_value(tree: Tree, path: Path, value: Scalar) -> Tree: | |
| """Copy only the selected ancestor chain, retaining the original semantic tree.""" | |
| if not path: | |
| return value | |
| head, *tail = path | |
| if isinstance(tree, dict) and isinstance(head, str) and head in tree: | |
| return {**tree, head: _with_value(tree[head], tuple(tail), value)} | |
| if isinstance(tree, list) and type(head) is int and 0 <= head < len(tree): | |
| result = tree.copy() | |
| result[head] = _with_value(tree[head], tuple(tail), value) | |
| return result | |
| raise YamlEditError( | |
| ErrorCode.INVARIANT_VIOLATION, "Discovered path does not match the semantic tree." | |
| ) | |
| def _plain_string(value: str) -> bool: | |
| # Avoid reinterpreting strings as booleans/numbers in either YAML 1.2 or | |
| # common YAML 1.1 consumers. Parsing never runs arbitrary constructors here. | |
| for version in ((1, 2), (1, 1)): | |
| parser = YAML(typ="safe", pure=True) | |
| parser.version = version | |
| node = parser.compose(value) | |
| if not isinstance(node, ScalarNode) or node.tag != "tag:yaml.org,2002:str": | |
| return False | |
| if node.value != value: | |
| return False | |
| return True | |
| def _replacement(value: Scalar, original: ScalarNode) -> str: | |
| if isinstance(value, str): | |
| if original.style == "'": | |
| return "'" + value.replace("'", "''") + "'" | |
| if original.style != '"' and _plain_string(value): | |
| return value | |
| return json.dumps(value, ensure_ascii=False, allow_nan=False) | |
| def _unified_diff(before: str, after: str) -> str: | |
| if before == after: | |
| return "" | |
| # Review output uses LF regardless of the source's newline convention. | |
| # The returned source itself remains byte-for-byte preserved outside the edit. | |
| lines = unified_diff( | |
| before.replace("\r\n", "\n").replace("\r", "\n").splitlines(keepends=True), | |
| after.replace("\r\n", "\n").replace("\r", "\n").splitlines(keepends=True), | |
| fromfile="before.yaml", | |
| tofile="after.yaml", | |
| ) | |
| # difflib doesn't supply the conventional missing-newline marker itself. | |
| return "".join( | |
| line if line.endswith("\n") else line + "\n\\ No newline at end of file\n" for line in lines | |
| ) | |
| def _propose_parsed(parsed: Parsed, source: str, candidate_id: str, value: object) -> EditProposal: | |
| """Create a proposal from a private, request-local parsed source snapshot.""" | |
| candidate = _candidate(parsed, candidate_id) | |
| proposal = EditProposal( | |
| source_version=parsed.source_version, | |
| candidate_id=candidate.id, | |
| path=candidate.path, | |
| expected_old_value=candidate.current_value, | |
| value=validate_value(candidate.kind, value), | |
| ) | |
| # Includes post-edit structure, request/limit, and invariant checks. | |
| _preview(parsed, source, proposal) | |
| return proposal | |
| def propose(source: str, candidate_id: str, value: object) -> EditProposal: | |
| """Create a validated proposal. No files are read or written.""" | |
| return _propose_parsed(parse_source(source), source, candidate_id, value) | |
| def _preview(parsed: Parsed, source: str, proposal: EditProposal) -> EditPreview: | |
| if parsed.source_version != proposal.source_version: | |
| raise YamlEditError( | |
| ErrorCode.STALE_PROPOSAL, "Source changed; generate and review a new proposal." | |
| ) | |
| candidate = _candidate(parsed, proposal.candidate_id) | |
| if candidate.path != proposal.path or not _same( | |
| candidate.current_value, proposal.expected_old_value | |
| ): | |
| raise YamlEditError( | |
| ErrorCode.INVALID_PROPOSAL, "Proposal does not match its allowlisted target." | |
| ) | |
| value = validate_value(candidate.kind, proposal.value) | |
| if _same(value, candidate.current_value): | |
| return EditPreview(source=source, diff="", changed=False, candidate=candidate) | |
| original = parsed.nodes[candidate.path] | |
| if not isinstance(original, ScalarNode): | |
| raise YamlEditError(ErrorCode.INVARIANT_VIOLATION, "Discovered target is not a scalar.") | |
| start, end = candidate.location.start, candidate.location.end | |
| result = source[:start] + _replacement(value, original) + source[end:] | |
| reparsed = parse_source(result) | |
| expected = _with_value(parsed.data, candidate.path, value) | |
| if not _same(expected, reparsed.data): | |
| raise YamlEditError( | |
| ErrorCode.INVARIANT_VIOLATION, "Edit changed more than the intended semantic value." | |
| ) | |
| return EditPreview( | |
| source=result, diff=_unified_diff(source, result), changed=True, candidate=candidate | |
| ) | |
| def preview(source: str, proposal: object) -> EditPreview: | |
| """Revalidate an untrusted proposal and return the exact source and review diff.""" | |
| checked = EditProposal.from_dict( | |
| proposal.to_dict() if isinstance(proposal, EditProposal) else proposal | |
| ) | |
| parsed = parse_source(source) | |
| return _preview(parsed, source, checked) | |
| def apply(source: str, proposal: object) -> str: | |
| """Apply to an immutable string snapshot; callers handle persistence/confirmation.""" | |
| return preview(source, proposal).source | |
| def _batch_invalid() -> Never: | |
| raise YamlEditError(ErrorCode.INVALID_PROPOSAL, "Malformed or unsupported batch request.") | |
| def _bounded_request_text(value: object, maximum: int, *, pointer: bool = False) -> str: | |
| if type(value) is not str or len(value) > maximum or (pointer and not value.startswith("/")): | |
| _batch_invalid() | |
| text = value | |
| try: | |
| raw = text.encode("utf-8") | |
| except UnicodeEncodeError as error: | |
| raise YamlEditError(ErrorCode.INVALID_PROPOSAL, "Malformed batch request text.") from error | |
| if len(raw) > maximum: | |
| _batch_invalid() | |
| return text | |
| def _batch_requests(value: object) -> tuple[BatchRequest, ...]: | |
| if type(value) not in (list, tuple): | |
| _batch_invalid() | |
| requests = cast("list[object] | tuple[object, ...]", value) | |
| if not 1 <= len(requests) <= MAX_BATCH_OPERATIONS: | |
| _batch_invalid() | |
| checked: list[BatchRequest] = [] | |
| for item in requests: | |
| if type(item) is not BatchRequest: | |
| _batch_invalid() | |
| candidate_value = cast("object", getattr(item, "candidate_id", None)) | |
| requested_value = cast("object", getattr(item, "value", None)) | |
| candidate_id = _bounded_request_text( | |
| candidate_value, MAX_BATCH_CANDIDATE_BYTES, pointer=True | |
| ) | |
| if not is_scalar(requested_value): | |
| _batch_invalid() | |
| if type(requested_value) is str: | |
| _bounded_request_text(requested_value, MAX_BATCH_SCALAR_BYTES) | |
| checked.append(BatchRequest(candidate_id, requested_value)) | |
| return tuple(checked) | |
| def _resolve_requests( | |
| parsed: Parsed, requests: tuple[BatchRequest, ...] | |
| ) -> tuple[tuple[Candidate, Scalar], ...]: | |
| seen: set[str] = set() | |
| resolved: list[tuple[Candidate, Scalar]] = [] | |
| for request in requests: | |
| if request.candidate_id in seen: | |
| _batch_invalid() | |
| seen.add(request.candidate_id) | |
| candidate = _candidate(parsed, request.candidate_id) | |
| resolved.append((candidate, validate_value(candidate.kind, request.value))) | |
| return tuple(sorted(resolved, key=lambda item: (item[0].location.start, item[0].id))) | |
| def _proposal_entries( | |
| resolved: tuple[tuple[Candidate, Scalar], ...], | |
| ) -> tuple[BatchEntry, ...]: | |
| return tuple( | |
| BatchEntry(candidate.id, candidate.path, candidate.current_value, value) | |
| for candidate, value in resolved | |
| ) | |
| def propose_batch(source: str, requests: Sequence[BatchRequest]) -> BatchProposal: | |
| """Create one validated proposal for explicit existing scalar targets.""" | |
| checked_requests = _batch_requests(requests) | |
| parsed = parse_source(source) | |
| resolved = _resolve_requests(parsed, checked_requests) | |
| proposal = BatchProposal(parsed.source_version, _proposal_entries(resolved)) | |
| _preview_batch(parsed, source, proposal) | |
| proposal.to_dict() | |
| return proposal | |
| def _entry_matches(candidate: Candidate, entry: BatchEntry) -> bool: | |
| return candidate.path == entry.path and _same(candidate.current_value, entry.expected_old_value) | |
| def _resolve_entries( | |
| parsed: Parsed, proposal: BatchProposal, source_length: int | |
| ) -> tuple[tuple[Candidate, Scalar], ...]: | |
| if parsed.source_version != proposal.source_version: | |
| raise YamlEditError( | |
| ErrorCode.STALE_PROPOSAL, "Source changed; generate and review a new batch proposal." | |
| ) | |
| ids: set[str] = set() | |
| paths: set[Path] = set() | |
| spans: set[tuple[int, int]] = set() | |
| resolved: list[tuple[Candidate, Scalar]] = [] | |
| for entry in proposal.entries: | |
| candidate = _candidate(parsed, entry.candidate_id) | |
| if not _entry_matches(candidate, entry): | |
| raise YamlEditError( | |
| ErrorCode.INVALID_PROPOSAL, "Batch entry does not match its allowlisted target." | |
| ) | |
| span = (candidate.location.start, candidate.location.end) | |
| if candidate.id in ids or candidate.path in paths or span in spans: | |
| raise YamlEditError(ErrorCode.INVALID_PROPOSAL, "Batch targets must be distinct.") | |
| ids.add(candidate.id) | |
| paths.add(candidate.path) | |
| spans.add(span) | |
| resolved.append((candidate, validate_value(candidate.kind, entry.value))) | |
| canonical = sorted(resolved, key=lambda item: (item[0].location.start, item[0].id)) | |
| if resolved != canonical: | |
| raise YamlEditError(ErrorCode.INVALID_PROPOSAL, "Batch entries are not in source order.") | |
| _check_batch_spans(resolved, source_length) | |
| return tuple(resolved) | |
| def _check_batch_spans(resolved: list[tuple[Candidate, Scalar]], source_length: int) -> None: | |
| previous_end = -1 | |
| for candidate, _value in resolved: | |
| location = candidate.location | |
| if not 0 <= location.start <= location.end <= source_length: | |
| raise YamlEditError( | |
| ErrorCode.INVARIANT_VIOLATION, "Discovered batch target span is invalid." | |
| ) | |
| if location.start < previous_end: | |
| raise YamlEditError( | |
| ErrorCode.INVARIANT_VIOLATION, "Discovered batch target spans overlap." | |
| ) | |
| previous_end = location.end | |
| def _batch_expected(parsed: Parsed, resolved: tuple[tuple[Candidate, Scalar], ...]) -> Tree: | |
| expected: Tree = parsed.data | |
| for candidate, value in resolved: | |
| expected = _with_value(expected, candidate.path, value) | |
| return expected | |
| def _batch_source( | |
| parsed: Parsed, source: str, resolved: tuple[tuple[Candidate, Scalar], ...] | |
| ) -> str: | |
| result = source | |
| for candidate, value in reversed(resolved): | |
| if _same(candidate.current_value, value): | |
| continue | |
| original = parsed.nodes.get(candidate.path) | |
| if not isinstance(original, ScalarNode): | |
| raise YamlEditError( | |
| ErrorCode.INVARIANT_VIOLATION, "Discovered batch target is not a scalar." | |
| ) | |
| start, end = candidate.location.start, candidate.location.end | |
| if not 0 <= start <= end <= len(source): | |
| raise YamlEditError( | |
| ErrorCode.INVARIANT_VIOLATION, "Discovered batch target span is invalid." | |
| ) | |
| result = result[:start] + _replacement(value, original) + result[end:] | |
| return result | |
| def _preview_batch(parsed: Parsed, source: str, proposal: BatchProposal) -> BatchPreview: | |
| resolved = _resolve_entries(parsed, proposal, len(source)) | |
| expected = _batch_expected(parsed, resolved) | |
| result = _batch_source(parsed, source, resolved) | |
| reparsed = parse_source(result) | |
| if not _same(expected, reparsed.data): | |
| raise YamlEditError( | |
| ErrorCode.INVARIANT_VIOLATION, | |
| "Batch edit changed more than the intended semantic values.", | |
| ) | |
| return BatchPreview( | |
| source=result, | |
| diff=_unified_diff(source, result), | |
| changed=result != source, | |
| candidates=tuple(candidate for candidate, _value in resolved), | |
| ) | |
| def preview_batch(source: str, proposal: object) -> BatchPreview: | |
| """Revalidate an untrusted batch artifact and return its complete review result.""" | |
| checked = validate_batch_proposal(proposal) | |
| parsed = parse_source(source) | |
| return _preview_batch(parsed, source, checked) | |
| def apply_batch(source: str, proposal: object) -> str: | |
| """Apply an atomic batch to an immutable string snapshot.""" | |
| return preview_batch(source, proposal).source | |