auto-code-fixer 0.3.6__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_code_fixer/__init__.py +1 -1
- auto_code_fixer/approval.py +114 -0
- auto_code_fixer/cli.py +760 -60
- auto_code_fixer/reporting.py +61 -0
- auto_code_fixer/sandbox.py +35 -0
- auto_code_fixer/utils.py +42 -0
- {auto_code_fixer-0.3.6.dist-info → auto_code_fixer-0.4.0.dist-info}/METADATA +58 -6
- {auto_code_fixer-0.3.6.dist-info → auto_code_fixer-0.4.0.dist-info}/RECORD +12 -10
- {auto_code_fixer-0.3.6.dist-info → auto_code_fixer-0.4.0.dist-info}/WHEEL +0 -0
- {auto_code_fixer-0.3.6.dist-info → auto_code_fixer-0.4.0.dist-info}/entry_points.txt +0 -0
- {auto_code_fixer-0.3.6.dist-info → auto_code_fixer-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {auto_code_fixer-0.3.6.dist-info → auto_code_fixer-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime as _dt
|
|
3
|
+
import difflib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def utc_now_iso() -> str:
|
|
10
|
+
return _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def unified_diff(*, old: str, new: str, fromfile: str, tofile: str) -> str:
|
|
14
|
+
return "".join(
|
|
15
|
+
difflib.unified_diff(
|
|
16
|
+
(old or "").splitlines(keepends=True),
|
|
17
|
+
(new or "").splitlines(keepends=True),
|
|
18
|
+
fromfile=fromfile,
|
|
19
|
+
tofile=tofile,
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclasses.dataclass
|
|
25
|
+
class ReportAttempt:
|
|
26
|
+
index: int
|
|
27
|
+
run_cmd: str
|
|
28
|
+
return_code: int = 0
|
|
29
|
+
stdout: str | None = None
|
|
30
|
+
stderr: str | None = None
|
|
31
|
+
selected_file: str | None = None
|
|
32
|
+
ruff_first_applied: bool = False
|
|
33
|
+
changed_files: list[str] = dataclasses.field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclasses.dataclass
|
|
37
|
+
class FixReport:
|
|
38
|
+
tool: str = "auto-code-fixer"
|
|
39
|
+
version: str | None = None
|
|
40
|
+
mode: str = "file" # file|fixpack
|
|
41
|
+
project_root: str | None = None
|
|
42
|
+
sandbox_root: str | None = None
|
|
43
|
+
target: str | None = None
|
|
44
|
+
run_cmd: str | None = None
|
|
45
|
+
started_at: str = dataclasses.field(default_factory=utc_now_iso)
|
|
46
|
+
finished_at: str | None = None
|
|
47
|
+
ok: bool | None = None
|
|
48
|
+
attempts: list[ReportAttempt] = dataclasses.field(default_factory=list)
|
|
49
|
+
files_touched: list[str] = dataclasses.field(default_factory=list)
|
|
50
|
+
diffs: dict[str, str] = dataclasses.field(default_factory=dict) # relpath -> unified diff
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
d = dataclasses.asdict(self)
|
|
54
|
+
# dataclasses.asdict converts nested dataclasses, OK
|
|
55
|
+
return d
|
|
56
|
+
|
|
57
|
+
def write_json(self, path: str) -> None:
|
|
58
|
+
os.makedirs(os.path.dirname(os.path.abspath(path)) or ".", exist_ok=True)
|
|
59
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
60
|
+
json.dump(self.to_dict(), f, indent=2, sort_keys=False)
|
|
61
|
+
f.write("\n")
|
auto_code_fixer/sandbox.py
CHANGED
|
@@ -35,6 +35,41 @@ def make_sandbox(*, entry_file: str, project_root: str) -> tuple[str, str]:
|
|
|
35
35
|
return sandbox_root, sandbox_entry
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def make_sandbox_project(*, project_root: str) -> str:
|
|
39
|
+
"""Create a sandbox directory with a full copy of project_root.
|
|
40
|
+
|
|
41
|
+
This is used for "Fixpack" runs where we execute a command (e.g., pytest) across
|
|
42
|
+
a package/folder and iteratively fix failures.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
project_root = os.path.abspath(project_root)
|
|
46
|
+
sandbox_root = tempfile.mkdtemp(prefix="codefix_sandbox_")
|
|
47
|
+
|
|
48
|
+
def _ignore(_dir: str, names: list[str]):
|
|
49
|
+
skip = {".git",
|
|
50
|
+
".venv",
|
|
51
|
+
"__pycache__",
|
|
52
|
+
"build",
|
|
53
|
+
"dist",
|
|
54
|
+
".mypy_cache",
|
|
55
|
+
".ruff_cache",
|
|
56
|
+
".pytest_cache",
|
|
57
|
+
}
|
|
58
|
+
return {n for n in names if n in skip}
|
|
59
|
+
|
|
60
|
+
# Copy everything into sandbox_root.
|
|
61
|
+
# Note: copytree requires dest not to exist; copy contents manually.
|
|
62
|
+
for item in os.listdir(project_root):
|
|
63
|
+
src = os.path.join(project_root, item)
|
|
64
|
+
dst = os.path.join(sandbox_root, item)
|
|
65
|
+
if os.path.isdir(src):
|
|
66
|
+
shutil.copytree(src, dst, ignore=_ignore)
|
|
67
|
+
else:
|
|
68
|
+
shutil.copy2(src, dst)
|
|
69
|
+
|
|
70
|
+
return sandbox_root
|
|
71
|
+
|
|
72
|
+
|
|
38
73
|
def apply_sandbox_back(*, sandbox_root: str, project_root: str, changed_paths: list[str]) -> None:
|
|
39
74
|
"""Copy changed sandbox files back into project_root."""
|
|
40
75
|
|
auto_code_fixer/utils.py
CHANGED
|
@@ -104,3 +104,45 @@ def discover_all_files(entry_file: str) -> list[str]:
|
|
|
104
104
|
|
|
105
105
|
def is_in_project(file_path: str, project_root: str) -> bool:
|
|
106
106
|
return os.path.abspath(file_path).startswith(os.path.abspath(project_root))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def sha256_text(text: str) -> str:
|
|
110
|
+
import hashlib
|
|
111
|
+
|
|
112
|
+
return hashlib.sha256((text or "").encode("utf-8")).hexdigest()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def snapshot_py_hashes(root: str) -> dict[str, str]:
|
|
116
|
+
"""Return mapping of absolute .py path -> sha256 of contents."""
|
|
117
|
+
|
|
118
|
+
import hashlib
|
|
119
|
+
|
|
120
|
+
out: dict[str, str] = {}
|
|
121
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
122
|
+
# Keep the walk light.
|
|
123
|
+
dirnames[:] = [
|
|
124
|
+
d
|
|
125
|
+
for d in dirnames
|
|
126
|
+
if d not in {".git", ".venv", "__pycache__", "build", "dist", ".mypy_cache", ".ruff_cache"}
|
|
127
|
+
]
|
|
128
|
+
for fn in filenames:
|
|
129
|
+
if not fn.endswith(".py"):
|
|
130
|
+
continue
|
|
131
|
+
ap = os.path.join(dirpath, fn)
|
|
132
|
+
try:
|
|
133
|
+
data = Path(ap).read_bytes()
|
|
134
|
+
except Exception:
|
|
135
|
+
continue
|
|
136
|
+
out[os.path.abspath(ap)] = hashlib.sha256(data).hexdigest()
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def changed_py_files(before: dict[str, str], after: dict[str, str]) -> set[str]:
|
|
141
|
+
"""Return absolute paths for .py files whose hash changed or were added/removed."""
|
|
142
|
+
|
|
143
|
+
changed: set[str] = set()
|
|
144
|
+
all_paths = set(before) | set(after)
|
|
145
|
+
for p in all_paths:
|
|
146
|
+
if before.get(p) != after.get(p):
|
|
147
|
+
changed.add(os.path.abspath(p))
|
|
148
|
+
return changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: auto-code-fixer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Automatically fix Python code using ChatGPT
|
|
5
5
|
Author-email: Arif Shah <ashah7775@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -87,6 +87,18 @@ pip install requests
|
|
|
87
87
|
Before overwriting any file, it creates a backup:
|
|
88
88
|
- `file.py.bak` (or `.bak1`, `.bak2`, ...)
|
|
89
89
|
|
|
90
|
+
### Approval mode (diff review)
|
|
91
|
+
```bash
|
|
92
|
+
auto-code-fixer path/to/main.py --project-root . --approve
|
|
93
|
+
```
|
|
94
|
+
In patch-protocol mode, approvals are **file-by-file** (apply/skip).
|
|
95
|
+
|
|
96
|
+
### Diff / size guards
|
|
97
|
+
To prevent huge edits from being applied accidentally:
|
|
98
|
+
- `--max-diff-lines` limits unified-diff size per file
|
|
99
|
+
- `--max-file-bytes` limits the proposed new content size per file
|
|
100
|
+
- `--max-total-bytes` limits total proposed new content across all files
|
|
101
|
+
|
|
90
102
|
### Dry run
|
|
91
103
|
```bash
|
|
92
104
|
auto-code-fixer path/to/main.py --project-root . --dry-run
|
|
@@ -96,6 +108,15 @@ auto-code-fixer path/to/main.py --project-root . --dry-run
|
|
|
96
108
|
|
|
97
109
|
## Advanced options
|
|
98
110
|
|
|
111
|
+
### Fixpack mode (folder/package runner)
|
|
112
|
+
Fixpack runs a command (default: `pytest -q`) against a **full-project sandbox**, then iteratively fixes the failing file(s) until green or you hit `--max-retries`.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
auto-code-fixer path/to/package --project-root . --fixpack --no-ask
|
|
116
|
+
# equivalent explicit command:
|
|
117
|
+
auto-code-fixer path/to/package --project-root . --fixpack --run "pytest -q" --no-ask
|
|
118
|
+
```
|
|
119
|
+
|
|
99
120
|
### Run a custom command (pytest, etc.)
|
|
100
121
|
Instead of `python main.py`, run tests:
|
|
101
122
|
|
|
@@ -103,6 +124,16 @@ Instead of `python main.py`, run tests:
|
|
|
103
124
|
auto-code-fixer . --project-root . --run "pytest -q" --no-ask
|
|
104
125
|
```
|
|
105
126
|
|
|
127
|
+
When you use `--run`, the tool (by default) also performs a **post-apply check**:
|
|
128
|
+
after copying fixes back to your project, it re-runs the same command against the real project files
|
|
129
|
+
(using the sandbox venv for dependencies).
|
|
130
|
+
|
|
131
|
+
You can disable that extra check with:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
auto-code-fixer . --project-root . --run "pytest -q" --no-post-apply-check
|
|
135
|
+
```
|
|
136
|
+
|
|
106
137
|
### Model selection
|
|
107
138
|
```bash
|
|
108
139
|
export AUTO_CODE_FIXER_MODEL=gpt-4.1-mini
|
|
@@ -121,13 +152,25 @@ auto-code-fixer main.py --ai-plan
|
|
|
121
152
|
```
|
|
122
153
|
This enables a helper that can suggest which local file to edit. It is best-effort.
|
|
123
154
|
|
|
124
|
-
###
|
|
155
|
+
### Structured patch protocol (JSON + sha256) (default)
|
|
156
|
+
By default, Auto Code Fixer uses a structured **patch protocol** where the model returns strict JSON:
|
|
157
|
+
|
|
158
|
+
`{ "files": [ {"path": "...", "new_content": "...", "sha256": "..."}, ... ] }`
|
|
159
|
+
|
|
160
|
+
The tool verifies the SHA-256 hash of `new_content` before applying edits.
|
|
161
|
+
|
|
162
|
+
To disable this and use legacy full-text mode only:
|
|
163
|
+
|
|
125
164
|
```bash
|
|
126
|
-
auto-code-fixer main.py --
|
|
165
|
+
auto-code-fixer main.py --legacy-mode
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Ruff-first mode (best-effort)
|
|
169
|
+
Before calling the LLM, you can try Ruff auto-fixes (including import sorting) and Ruff formatting:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
auto-code-fixer path/to/package --project-root . --fixpack --ruff-first
|
|
127
173
|
```
|
|
128
|
-
When enabled, the model is asked to return strict JSON with `{files:[{path,new_content,sha256}]}`.
|
|
129
|
-
The tool verifies the SHA-256 hash of `new_content` before applying edits, and falls back to the
|
|
130
|
-
legacy full-text mode if parsing/validation fails.
|
|
131
174
|
|
|
132
175
|
### Optional formatting / linting (best-effort)
|
|
133
176
|
```bash
|
|
@@ -136,6 +179,15 @@ auto-code-fixer main.py --lint ruff --fix
|
|
|
136
179
|
```
|
|
137
180
|
These run inside the sandbox venv and are skipped if the tools are not installed.
|
|
138
181
|
|
|
182
|
+
### JSON report (explain)
|
|
183
|
+
Write a machine-readable report with attempts, files touched, and unified diffs:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
auto-code-fixer main.py --explain
|
|
187
|
+
# or
|
|
188
|
+
auto-code-fixer main.py --report /tmp/report.json
|
|
189
|
+
```
|
|
190
|
+
|
|
139
191
|
---
|
|
140
192
|
|
|
141
193
|
## Environment variables
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
auto_code_fixer/__init__.py,sha256=
|
|
2
|
-
auto_code_fixer/
|
|
1
|
+
auto_code_fixer/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
|
|
2
|
+
auto_code_fixer/approval.py,sha256=NPSLu54maAK2RAJF_t4fq7Bs_M8K026DhDwqBFJtiwQ,2832
|
|
3
|
+
auto_code_fixer/cli.py,sha256=Dn9A2geL58lvVWLt6K0whZfZVcBN3zpWEM5OS38Q4Ik,47713
|
|
3
4
|
auto_code_fixer/command_runner.py,sha256=6P8hGRavN5C39x-e03p02Vc805NnZH9U7e48ngb5jJI,1104
|
|
4
5
|
auto_code_fixer/fixer.py,sha256=zcgw56pRTuOLvna09lTXatD0VWwjjzBVk0OyEKfgxDM,4691
|
|
5
6
|
auto_code_fixer/installer.py,sha256=LC0jasSsPI7eHMeDxa622OoMCR1951HAXUZWp-kcmVY,1522
|
|
@@ -7,14 +8,15 @@ auto_code_fixer/models.py,sha256=JLBJutOoiOjjlT_RMPUPhWlmm1yc_nGcQqv5tY72Al0,317
|
|
|
7
8
|
auto_code_fixer/patch_protocol.py,sha256=8l1E9o-3jkO4VAI7Ulrf-1MbAshNzjQXtUkmH-0hYio,3216
|
|
8
9
|
auto_code_fixer/patcher.py,sha256=BcQTnjWazdpuEXyR2AlumFBzIk_yIrO3fGTaIqpHuiU,1811
|
|
9
10
|
auto_code_fixer/plan.py,sha256=jrZdG-f1RDxVB0tBLlTwKbCSEiOYI_RMetdzfBcyE4s,1762
|
|
11
|
+
auto_code_fixer/reporting.py,sha256=bulUsP0DIfLBmtVjZ1YdTV7RAk8Aiy81ASavcr4iano,1887
|
|
10
12
|
auto_code_fixer/runner.py,sha256=BvQm3CrwkQEDOw0tpiamSTcdu3OjbOgA801xW2zWdP8,970
|
|
11
|
-
auto_code_fixer/sandbox.py,sha256=
|
|
13
|
+
auto_code_fixer/sandbox.py,sha256=s36i7mzZFjuAWRUEumxtYfVwasFa8TehobzH-rE_kKA,2721
|
|
12
14
|
auto_code_fixer/traceback_utils.py,sha256=sbSuLO-2UBk5QPJZYJunTK9WGOpEY8mxR6WRKbtCIoM,935
|
|
13
|
-
auto_code_fixer/utils.py,sha256=
|
|
15
|
+
auto_code_fixer/utils.py,sha256=JLagGVnUj67zZdpkKSHhVcqwob30f62SyS7IbH20sHs,4369
|
|
14
16
|
auto_code_fixer/venv_manager.py,sha256=2ww8reYgLbLohh-moAD5YKM09qv_mC5yYzJRwm3XiXc,1202
|
|
15
|
-
auto_code_fixer-0.
|
|
16
|
-
auto_code_fixer-0.
|
|
17
|
-
auto_code_fixer-0.
|
|
18
|
-
auto_code_fixer-0.
|
|
19
|
-
auto_code_fixer-0.
|
|
20
|
-
auto_code_fixer-0.
|
|
17
|
+
auto_code_fixer-0.4.0.dist-info/licenses/LICENSE,sha256=hgchJNa26tjXuLztwSUDbYQxNLnAPnLk6kDXNIkC8xc,1066
|
|
18
|
+
auto_code_fixer-0.4.0.dist-info/METADATA,sha256=qyjUMCYrF1_wRp_4LQSa6t1btgwTNslKTl9rBNhSdZo,5570
|
|
19
|
+
auto_code_fixer-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
+
auto_code_fixer-0.4.0.dist-info/entry_points.txt,sha256=a-j2rkfwkrhXZ5Qbz_6_gwk6Bj7nijYR1DALjWp5Myk,61
|
|
21
|
+
auto_code_fixer-0.4.0.dist-info/top_level.txt,sha256=qUk1qznb6Qxqmxy2A3z_5dpOZlmNKHwUiLuJwH-CrAk,16
|
|
22
|
+
auto_code_fixer-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|