Skip to content

Commit 1f43a61

Browse files
committed
fix(patch): make --apply safe on descriptors, fallbacks and backups
Follow-up to the AST patcher, addressing review findings. Correctness: - _wrap_pytest_test captured the target with getattr, which unwraps a staticmethod/classmethod. Every verification of such a method raised TypeError, and "restoring" it left the class permanently mis-bound. Capture with inspect.getattr_static and re-wrap the candidate. - Validate the spliced result on the textual fallback path too, not just the AST path. When the file was already unparseable the replacement is checked on its own, so a repair to a broken file is still possible. - Re-indent the replacement in _textual_replace; a repair dedented to column 0 used to corrupt the enclosing block. - Refuse a textual match that occurs more than once instead of patching the first one. - Never overwrite an existing .heal-backup; later runs get a numbered suffix so the pristine copy survives. - Preserve the file's line endings instead of round-tripping them through read_text/write_text. - Take only the repaired function's own lines, and refuse a repair that defines anything alongside it: splicing put those in the target's scope, dropping them left a NameError at call time. - Keep a missing trailing newline missing. Reporting: - apply_function_patch returns PatchResult(src_path, backup_path, notes) so "no backup written" cannot read as a backup filename, and keeping decorators the repair omitted is reported rather than silent. - Warn instead of swallowing failures to restore a patched reference or to scan a module for aliases. Tests: 17 new, covering descriptor re-wrap and subclass cls binding, the changed-decorator branch, closures via outer.inner, dotted misses, backup versioning, fallback re-indent/ambiguity/validation, CRLF round trip and trailing newlines. Both xfail markers are fixed and removed. The old fallback test used `this is not python` as its "unparseable" fixture, which parses fine as an `is not` comparison, so the fallback path had never actually run.
1 parent 0c9b7c0 commit 1f43a61

7 files changed

Lines changed: 1120 additions & 130 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ pytest --heal # print proposed fix, leave files untouched
205205
pytest --heal-apply # write the fix back to disk (creates a .py.heal-backup)
206206
pytest --heal-apply-force # also allow modification of git-dirty files
207207
```
208-
`--heal-apply` uses libcst for AST-faithful replacement when installed, falling back to textual replacement. It refuses to modify files with uncommitted git changes unless `--heal-apply-force` is given.
208+
`--heal-apply` locates the target function with Python's `ast` module and overwrites only that definition's lines, falling back to textual replacement when the file cannot be parsed or the target is not found in it. Either way the result is re-parsed before anything is written. It refuses to modify files with uncommitted git changes unless `--heal-apply-force` is given, and never overwrites an existing `.heal-backup`.
209209

210210
### CLI: heal a function from the command line
211211
```bash

site/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export default function Home() {
126126
<Feature
127127
icon={<GitBranch className="h-5 w-5" />}
128128
title="pytest --heal-apply"
129-
body="Your failing tests trigger repair. libcst writes the accepted fix back to the file with a git-dirty guard and backup."
129+
body="Your failing tests trigger repair. The accepted fix is written back to the file with a git-dirty guard and backup."
130130
/>
131131
<Feature
132132
icon={<Activity className="h-5 w-5" />}

0 commit comments

Comments
 (0)