auto-code-fixer 0.2.6__py3-none-any.whl → 0.3.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/cli.py +141 -33
- auto_code_fixer/command_runner.py +45 -0
- auto_code_fixer/fixer.py +57 -33
- auto_code_fixer/installer.py +38 -7
- auto_code_fixer/models.py +19 -0
- auto_code_fixer/patcher.py +31 -0
- auto_code_fixer/plan.py +61 -0
- auto_code_fixer/runner.py +27 -3
- auto_code_fixer/sandbox.py +49 -0
- auto_code_fixer/traceback_utils.py +27 -0
- auto_code_fixer/utils.py +68 -19
- auto_code_fixer/venv_manager.py +33 -0
- auto_code_fixer-0.3.0.dist-info/METADATA +138 -0
- auto_code_fixer-0.3.0.dist-info/RECORD +19 -0
- auto_code_fixer-0.2.6.dist-info/METADATA +0 -51
- auto_code_fixer-0.2.6.dist-info/RECORD +0 -12
- {auto_code_fixer-0.2.6.dist-info → auto_code_fixer-0.3.0.dist-info}/WHEEL +0 -0
- {auto_code_fixer-0.2.6.dist-info → auto_code_fixer-0.3.0.dist-info}/entry_points.txt +0 -0
- {auto_code_fixer-0.2.6.dist-info → auto_code_fixer-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {auto_code_fixer-0.2.6.dist-info → auto_code_fixer-0.3.0.dist-info}/top_level.txt +0 -0
auto_code_fixer/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.3.0"
|
auto_code_fixer/cli.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import os
|
|
2
3
|
import shutil
|
|
3
4
|
import tempfile
|
|
4
5
|
import time
|
|
@@ -15,23 +16,49 @@ from auto_code_fixer.utils import (
|
|
|
15
16
|
)
|
|
16
17
|
from auto_code_fixer import __version__
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
DEFAULT_MAX_RETRIES = 8 # can be overridden via CLI
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def fix_file(file_path, project_root, api_key, ask, verbose):
|
|
22
|
-
log(f"Processing file: {file_path}")
|
|
22
|
+
def fix_file(file_path, project_root, api_key, ask, verbose, *, dry_run: bool, model: str | None, timeout_s: int, max_retries: int, run_cmd: str | None) -> bool:
|
|
23
|
+
log(f"Processing entry file: {file_path}")
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
project_root = os.path.abspath(project_root)
|
|
26
|
+
file_path = os.path.abspath(file_path)
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# Build sandbox with entry + imported local files
|
|
29
|
+
from auto_code_fixer.sandbox import make_sandbox
|
|
30
|
+
sandbox_root, sandbox_entry = make_sandbox(entry_file=file_path, project_root=project_root)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
# Create isolated venv in sandbox
|
|
33
|
+
from auto_code_fixer.venv_manager import create_venv
|
|
34
|
+
from auto_code_fixer.patcher import backup_file
|
|
35
|
+
|
|
36
|
+
venv_python = create_venv(sandbox_root)
|
|
37
|
+
|
|
38
|
+
changed_sandbox_files: set[str] = set()
|
|
39
|
+
|
|
40
|
+
for attempt in range(max_retries):
|
|
32
41
|
log(f"Run attempt #{attempt + 1}")
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
# Ensure local modules resolve inside sandbox
|
|
44
|
+
if run_cmd:
|
|
45
|
+
from auto_code_fixer.command_runner import run_command
|
|
46
|
+
|
|
47
|
+
retcode, stdout, stderr = run_command(
|
|
48
|
+
run_cmd,
|
|
49
|
+
timeout_s=timeout_s,
|
|
50
|
+
python_exe=venv_python,
|
|
51
|
+
cwd=sandbox_root,
|
|
52
|
+
extra_env={"PYTHONPATH": sandbox_root},
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
retcode, stdout, stderr = run_code(
|
|
56
|
+
sandbox_entry,
|
|
57
|
+
timeout_s=timeout_s,
|
|
58
|
+
python_exe=venv_python,
|
|
59
|
+
cwd=sandbox_root,
|
|
60
|
+
extra_env={"PYTHONPATH": sandbox_root},
|
|
61
|
+
)
|
|
35
62
|
|
|
36
63
|
if verbose:
|
|
37
64
|
if stdout:
|
|
@@ -42,56 +69,101 @@ def fix_file(file_path, project_root, api_key, ask, verbose):
|
|
|
42
69
|
if retcode == 0:
|
|
43
70
|
log("Script executed successfully ✅")
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
# Apply sandbox changes back to project (only if we actually changed something)
|
|
73
|
+
if attempt > 0 and is_in_project(file_path, project_root) and changed_sandbox_files:
|
|
74
|
+
rel_changes = [os.path.relpath(p, sandbox_root) for p in sorted(changed_sandbox_files)]
|
|
75
|
+
|
|
46
76
|
if ask:
|
|
47
77
|
confirm = input(
|
|
48
|
-
|
|
78
|
+
"Overwrite original files with fixed versions?\n"
|
|
79
|
+
+ "\n".join(f"- {c}" for c in rel_changes)
|
|
80
|
+
+ "\n(y/n): "
|
|
49
81
|
).strip().lower()
|
|
50
82
|
|
|
51
83
|
if confirm != "y":
|
|
52
84
|
log("User declined overwrite", "WARN")
|
|
53
|
-
shutil.rmtree(
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
shutil.rmtree(sandbox_root)
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
if dry_run:
|
|
89
|
+
log("DRY RUN: would apply fixes:\n" + "\n".join(rel_changes), "WARN")
|
|
90
|
+
else:
|
|
91
|
+
for p in sorted(changed_sandbox_files):
|
|
92
|
+
rel = os.path.relpath(p, sandbox_root)
|
|
93
|
+
dst = os.path.join(project_root, rel)
|
|
94
|
+
if os.path.exists(dst):
|
|
95
|
+
bak = backup_file(dst)
|
|
96
|
+
log(f"Backup created: {bak}", "DEBUG")
|
|
97
|
+
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
98
|
+
shutil.copy(p, dst)
|
|
99
|
+
log(f"File updated: {dst}")
|
|
100
|
+
|
|
101
|
+
shutil.rmtree(sandbox_root)
|
|
60
102
|
log(f"Fix completed in {attempt + 1} attempt(s) 🎉")
|
|
61
|
-
return
|
|
103
|
+
return True
|
|
62
104
|
|
|
63
105
|
log("Error detected ❌", "ERROR")
|
|
64
106
|
print(stderr)
|
|
65
107
|
|
|
66
|
-
if check_and_install_missing_lib(stderr):
|
|
67
|
-
log("Missing dependency installed, retrying…")
|
|
108
|
+
if check_and_install_missing_lib(stderr, python_exe=venv_python, project_root=sandbox_root):
|
|
109
|
+
log("Missing dependency installed (venv), retrying…")
|
|
68
110
|
time.sleep(1)
|
|
69
111
|
continue
|
|
70
112
|
|
|
71
|
-
|
|
113
|
+
# Pick the most relevant local file from the traceback (entry or imported file)
|
|
114
|
+
from auto_code_fixer.traceback_utils import pick_relevant_file
|
|
115
|
+
|
|
116
|
+
target_file = pick_relevant_file(stderr, sandbox_root=sandbox_root) or sandbox_entry
|
|
117
|
+
|
|
118
|
+
# Optional AI fix plan can override file selection
|
|
119
|
+
try:
|
|
120
|
+
from auto_code_fixer.plan import ask_ai_for_fix_plan
|
|
121
|
+
|
|
122
|
+
plan = ask_ai_for_fix_plan(
|
|
123
|
+
sandbox_root=sandbox_root,
|
|
124
|
+
stderr=stderr,
|
|
125
|
+
api_key=api_key,
|
|
126
|
+
model=model,
|
|
127
|
+
)
|
|
128
|
+
if plan and plan.target_files:
|
|
129
|
+
# Use the first suggested file that exists
|
|
130
|
+
for rel in plan.target_files:
|
|
131
|
+
cand = os.path.abspath(os.path.join(sandbox_root, rel))
|
|
132
|
+
if cand.startswith(os.path.abspath(sandbox_root)) and os.path.exists(cand):
|
|
133
|
+
target_file = cand
|
|
134
|
+
break
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
log(f"Sending {os.path.relpath(target_file, sandbox_root)} + error to GPT 🧠", "DEBUG")
|
|
139
|
+
|
|
72
140
|
fixed_code = fix_code_with_gpt(
|
|
73
|
-
open(
|
|
74
|
-
stderr,
|
|
75
|
-
api_key,
|
|
141
|
+
original_code=open(target_file, encoding="utf-8").read(),
|
|
142
|
+
error_log=stderr,
|
|
143
|
+
api_key=api_key,
|
|
144
|
+
model=model,
|
|
76
145
|
)
|
|
77
146
|
|
|
78
|
-
if fixed_code.strip() == open(
|
|
147
|
+
if fixed_code.strip() == open(target_file, encoding="utf-8").read().strip():
|
|
79
148
|
log("GPT returned no changes. Stopping.", "WARN")
|
|
80
149
|
break
|
|
81
150
|
|
|
82
|
-
with open(
|
|
151
|
+
with open(target_file, "w", encoding="utf-8") as f:
|
|
83
152
|
f.write(fixed_code)
|
|
84
153
|
|
|
154
|
+
changed_sandbox_files.add(os.path.abspath(target_file))
|
|
155
|
+
|
|
85
156
|
log("Code updated by GPT ✏️")
|
|
86
157
|
time.sleep(1)
|
|
87
158
|
|
|
88
159
|
log("Failed to auto-fix file after max retries ❌", "ERROR")
|
|
89
|
-
shutil.rmtree(
|
|
160
|
+
shutil.rmtree(sandbox_root)
|
|
161
|
+
return False
|
|
90
162
|
|
|
91
163
|
|
|
92
164
|
def main():
|
|
93
165
|
parser = argparse.ArgumentParser(
|
|
94
|
-
description="Auto-fix Python code using
|
|
166
|
+
description="Auto-fix Python code using OpenAI (advanced sandbox + retry loop)"
|
|
95
167
|
)
|
|
96
168
|
|
|
97
169
|
parser.add_argument(
|
|
@@ -108,6 +180,23 @@ def main():
|
|
|
108
180
|
|
|
109
181
|
parser.add_argument("--project-root", default=".")
|
|
110
182
|
parser.add_argument("--api-key")
|
|
183
|
+
parser.add_argument("--model", default=None, help="OpenAI model (default: AUTO_CODE_FIXER_MODEL or gpt-4.1-mini)")
|
|
184
|
+
parser.add_argument("--timeout", type=int, default=30, help="Execution timeout seconds (default: 30)")
|
|
185
|
+
parser.add_argument("--dry-run", action="store_true", help="Do not overwrite files; just report what would change")
|
|
186
|
+
parser.add_argument("--max-retries", type=int, default=DEFAULT_MAX_RETRIES, help="Max fix attempts (default: 8)")
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
"--run",
|
|
189
|
+
default=None,
|
|
190
|
+
help=(
|
|
191
|
+
"Optional command to run instead of `python entry.py`. Examples: 'pytest -q', 'python -m module'. "
|
|
192
|
+
"If set, it runs inside the sandbox venv."
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"--ai-plan",
|
|
197
|
+
action="store_true",
|
|
198
|
+
help="Optional: use AI to suggest which file to edit (AUTO_CODE_FIXER_AI_PLAN=1)",
|
|
199
|
+
)
|
|
111
200
|
|
|
112
201
|
# ✅ Proper boolean flags
|
|
113
202
|
ask_group = parser.add_mutually_exclusive_group()
|
|
@@ -148,6 +237,25 @@ def main():
|
|
|
148
237
|
verbose = args.verbose or env_verbose
|
|
149
238
|
set_verbose(verbose)
|
|
150
239
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
240
|
+
# Optional: enable AI planning helper
|
|
241
|
+
if args.ai_plan:
|
|
242
|
+
os.environ["AUTO_CODE_FIXER_AI_PLAN"] = "1"
|
|
243
|
+
|
|
244
|
+
ok = fix_file(
|
|
245
|
+
args.entry_file,
|
|
246
|
+
args.project_root,
|
|
247
|
+
args.api_key,
|
|
248
|
+
ask,
|
|
249
|
+
verbose,
|
|
250
|
+
dry_run=args.dry_run,
|
|
251
|
+
model=args.model,
|
|
252
|
+
timeout_s=args.timeout,
|
|
253
|
+
max_retries=args.max_retries,
|
|
254
|
+
run_cmd=args.run,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
raise SystemExit(0 if ok else 2)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shlex
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_command(
|
|
7
|
+
cmd: str,
|
|
8
|
+
*,
|
|
9
|
+
timeout_s: int,
|
|
10
|
+
python_exe: str,
|
|
11
|
+
cwd: str,
|
|
12
|
+
extra_env: dict | None = None,
|
|
13
|
+
):
|
|
14
|
+
"""Run an arbitrary command inside the sandbox.
|
|
15
|
+
|
|
16
|
+
If cmd starts with 'python', replace with the sandbox venv python.
|
|
17
|
+
If cmd starts with 'pytest', run as `python -m pytest ...`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
parts = shlex.split(cmd)
|
|
21
|
+
if not parts:
|
|
22
|
+
return -1, "", "Empty command"
|
|
23
|
+
|
|
24
|
+
if parts[0] == "python" or parts[0] == "python3":
|
|
25
|
+
parts[0] = python_exe
|
|
26
|
+
|
|
27
|
+
if parts[0] == "pytest":
|
|
28
|
+
parts = [python_exe, "-m", "pytest", *parts[1:]]
|
|
29
|
+
|
|
30
|
+
env = {**os.environ}
|
|
31
|
+
if extra_env:
|
|
32
|
+
env.update({k: str(v) for k, v in extra_env.items()})
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
r = subprocess.run(
|
|
36
|
+
parts,
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
timeout=timeout_s,
|
|
40
|
+
cwd=cwd,
|
|
41
|
+
env=env,
|
|
42
|
+
)
|
|
43
|
+
return r.returncode, r.stdout, r.stderr
|
|
44
|
+
except subprocess.TimeoutExpired:
|
|
45
|
+
return -1, "", f"TimeoutExpired: Command took too long to run ({timeout_s}s)."
|
auto_code_fixer/fixer.py
CHANGED
|
@@ -1,55 +1,79 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
|
-
import time
|
|
3
3
|
from decouple import config
|
|
4
4
|
from openai import OpenAI
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def get_openai_client(api_key=None):
|
|
8
|
-
key = (
|
|
9
|
-
api_key
|
|
10
|
-
or os.getenv("OPENAI_API_KEY")
|
|
11
|
-
or config("OPENAI_API_KEY", default=None)
|
|
12
|
-
)
|
|
13
|
-
|
|
7
|
+
def get_openai_client(api_key: str | None = None) -> OpenAI:
|
|
8
|
+
key = api_key or os.getenv("OPENAI_API_KEY") or config("OPENAI_API_KEY", default=None)
|
|
14
9
|
if not key:
|
|
15
10
|
raise RuntimeError(
|
|
16
|
-
"OpenAI API key not found. "
|
|
17
|
-
"Set OPENAI_API_KEY env var or .env file or pass --api-key"
|
|
11
|
+
"OpenAI API key not found. Set OPENAI_API_KEY env var or .env file or pass --api-key"
|
|
18
12
|
)
|
|
19
|
-
|
|
20
13
|
return OpenAI(api_key=key)
|
|
21
14
|
|
|
22
15
|
|
|
23
|
-
def
|
|
24
|
-
|
|
16
|
+
def _strip_code_fences(text: str) -> str:
|
|
17
|
+
text = (text or "").strip()
|
|
18
|
+
if text.startswith("```"):
|
|
19
|
+
text = "\n".join(text.split("\n")[1:])
|
|
20
|
+
if text.endswith("```"):
|
|
21
|
+
text = "\n".join(text.split("\n")[:-1])
|
|
22
|
+
return text.strip()
|
|
23
|
+
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
def fix_code_with_gpt(
|
|
26
|
+
*,
|
|
27
|
+
original_code: str,
|
|
28
|
+
error_log: str,
|
|
29
|
+
api_key: str | None = None,
|
|
30
|
+
model: str | None = None,
|
|
31
|
+
) -> str:
|
|
32
|
+
"""Fix code using OpenAI. Returns full corrected code.
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
Model can be set via:
|
|
35
|
+
- --model
|
|
36
|
+
- AUTO_CODE_FIXER_MODEL env
|
|
37
|
+
- default fallback
|
|
38
|
+
"""
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
client = get_openai_client(api_key)
|
|
41
|
+
model = model or os.getenv("AUTO_CODE_FIXER_MODEL") or "gpt-4.1-mini"
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
"""
|
|
43
|
+
instructions = {
|
|
44
|
+
"task": "Fix Python code to run without errors.",
|
|
45
|
+
"requirements": [
|
|
46
|
+
"Preserve existing behavior unless required to fix the crash.",
|
|
47
|
+
"Do not remove functionality.",
|
|
48
|
+
"Return ONLY the full corrected Python source code (no markdown).",
|
|
49
|
+
],
|
|
50
|
+
}
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
prompt = (
|
|
53
|
+
"You are a senior Python engineer. Fix the following Python code so it runs without errors.\n\n"
|
|
54
|
+
"INSTRUCTIONS (JSON):\n"
|
|
55
|
+
+ json.dumps(instructions)
|
|
56
|
+
+ "\n\nCODE:\n"
|
|
57
|
+
+ original_code
|
|
58
|
+
+ "\n\nERROR LOG:\n"
|
|
59
|
+
+ error_log
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Use Responses API (openai>=1) for forward compatibility
|
|
63
|
+
resp = client.responses.create(
|
|
64
|
+
model=model,
|
|
65
|
+
input=[
|
|
42
66
|
{"role": "system", "content": "You fix broken Python code."},
|
|
43
67
|
{"role": "user", "content": prompt},
|
|
44
68
|
],
|
|
45
|
-
|
|
69
|
+
temperature=0.2,
|
|
70
|
+
max_output_tokens=2000,
|
|
46
71
|
)
|
|
47
72
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
fixed_code = "\n".join(fixed_code.split("\n")[:-1])
|
|
73
|
+
text = ""
|
|
74
|
+
for item in resp.output or []:
|
|
75
|
+
for c in item.content or []:
|
|
76
|
+
if getattr(c, "type", None) in ("output_text", "text"):
|
|
77
|
+
text += getattr(c, "text", "") or ""
|
|
54
78
|
|
|
55
|
-
return
|
|
79
|
+
return _strip_code_fences(text)
|
auto_code_fixer/installer.py
CHANGED
|
@@ -1,16 +1,47 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import subprocess
|
|
3
|
-
import sys
|
|
4
3
|
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
_MISSING_RE = re.compile(r"No module named '([^']+)'")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_missing_module(stderr: str) -> str | None:
|
|
9
|
+
m = _MISSING_RE.search(stderr or "")
|
|
10
|
+
if not m:
|
|
11
|
+
return None
|
|
12
|
+
return m.group(1)
|
|
13
|
+
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
def install_package(package: str, *, python_exe: str) -> bool:
|
|
12
16
|
try:
|
|
13
|
-
subprocess.check_call([
|
|
17
|
+
subprocess.check_call([python_exe, "-m", "pip", "install", package])
|
|
14
18
|
return True
|
|
15
19
|
except subprocess.CalledProcessError:
|
|
16
20
|
return False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_local_module(module: str, *, project_root: str) -> bool:
|
|
24
|
+
"""Return True if `module` can be resolved to a local file/package under project_root."""
|
|
25
|
+
import os
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
base = Path(project_root)
|
|
29
|
+
parts = module.split(".")
|
|
30
|
+
candidates = [
|
|
31
|
+
base.joinpath(*parts).with_suffix(".py"),
|
|
32
|
+
base.joinpath(*parts, "__init__.py"),
|
|
33
|
+
]
|
|
34
|
+
return any(os.path.isfile(str(p)) for p in candidates)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_and_install_missing_lib(stderr: str, *, python_exe: str, project_root: str) -> bool:
|
|
38
|
+
lib = parse_missing_module(stderr)
|
|
39
|
+
if not lib:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
# ✅ Important: do NOT pip install if it's a local module.
|
|
43
|
+
if is_local_module(lib, project_root=project_root):
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
# naive mapping; user can always fix the requirement name manually.
|
|
47
|
+
return install_package(lib, python_exe=python_exe)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class RunResult:
|
|
8
|
+
returncode: int
|
|
9
|
+
stdout: str
|
|
10
|
+
stderr: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class FixResult:
|
|
15
|
+
success: bool
|
|
16
|
+
attempts: int
|
|
17
|
+
changed_files: list[str]
|
|
18
|
+
last_error: str | None = None
|
|
19
|
+
sandbox_root: str | None = None
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class FileEdit:
|
|
7
|
+
path: str
|
|
8
|
+
new_content: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def safe_write(path: str, content: str) -> None:
|
|
12
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
13
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
14
|
+
f.write(content)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def safe_read(path: str) -> str:
|
|
18
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
19
|
+
return f.read()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def backup_file(path: str) -> str:
|
|
23
|
+
bak = path + ".bak"
|
|
24
|
+
# Avoid overwriting an existing bak
|
|
25
|
+
i = 1
|
|
26
|
+
while os.path.exists(bak):
|
|
27
|
+
bak = f"{path}.bak{i}"
|
|
28
|
+
i += 1
|
|
29
|
+
with open(path, "rb") as src, open(bak, "wb") as dst:
|
|
30
|
+
dst.write(src.read())
|
|
31
|
+
return bak
|
auto_code_fixer/plan.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from auto_code_fixer.fixer import get_openai_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class FixPlan:
|
|
10
|
+
target_files: list[str]
|
|
11
|
+
reason: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def ask_ai_for_fix_plan(*, sandbox_root: str, stderr: str, api_key: str | None, model: str | None) -> FixPlan | None:
|
|
15
|
+
"""Optional: let AI propose which file to edit.
|
|
16
|
+
|
|
17
|
+
This is a best-effort helper; errors should be ignored by caller.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
if os.getenv("AUTO_CODE_FIXER_AI_PLAN", "0").lower() not in {"1", "true", "yes"}:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
client = get_openai_client(api_key)
|
|
24
|
+
model = model or os.getenv("AUTO_CODE_FIXER_MODEL") or "gpt-4.1-mini"
|
|
25
|
+
|
|
26
|
+
prompt = (
|
|
27
|
+
"Given this Python traceback, decide which local file(s) should be edited to fix it.\n"
|
|
28
|
+
"Return ONLY JSON with keys: target_files (list of relative paths), reason (string).\n"
|
|
29
|
+
f"Sandbox root: {sandbox_root}\n\n"
|
|
30
|
+
"Traceback:\n" + (stderr or "")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
resp = client.responses.create(
|
|
34
|
+
model=model,
|
|
35
|
+
input=[{"role": "user", "content": prompt}],
|
|
36
|
+
temperature=0.0,
|
|
37
|
+
max_output_tokens=600,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
text = ""
|
|
41
|
+
for item in resp.output or []:
|
|
42
|
+
for c in item.content or []:
|
|
43
|
+
if getattr(c, "type", None) in ("output_text", "text"):
|
|
44
|
+
text += getattr(c, "text", "") or ""
|
|
45
|
+
|
|
46
|
+
text = (text or "").strip()
|
|
47
|
+
if not text:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
data = json.loads(text)
|
|
51
|
+
if not isinstance(data, dict):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
target_files = data.get("target_files") or []
|
|
55
|
+
if not isinstance(target_files, list):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
target_files = [str(x) for x in target_files if x]
|
|
59
|
+
reason = str(data.get("reason") or "")
|
|
60
|
+
|
|
61
|
+
return FixPlan(target_files=target_files, reason=reason)
|
auto_code_fixer/runner.py
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import subprocess
|
|
3
|
+
import sys
|
|
2
4
|
|
|
3
5
|
|
|
4
|
-
def run_code(
|
|
6
|
+
def run_code(
|
|
7
|
+
path_to_code: str,
|
|
8
|
+
*,
|
|
9
|
+
timeout_s: int = 30,
|
|
10
|
+
python_exe: str | None = None,
|
|
11
|
+
cwd: str | None = None,
|
|
12
|
+
extra_env: dict | None = None,
|
|
13
|
+
):
|
|
14
|
+
"""Run a python file and capture output.
|
|
15
|
+
|
|
16
|
+
python_exe: allow running inside a venv interpreter.
|
|
17
|
+
cwd: working directory (important for relative imports/files)
|
|
18
|
+
extra_env: extra environment variables (merged)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
exe = python_exe or sys.executable
|
|
22
|
+
env = {**os.environ}
|
|
23
|
+
if extra_env:
|
|
24
|
+
env.update({k: str(v) for k, v in extra_env.items()})
|
|
25
|
+
|
|
5
26
|
try:
|
|
6
27
|
result = subprocess.run(
|
|
7
|
-
[
|
|
28
|
+
[exe, path_to_code],
|
|
8
29
|
capture_output=True,
|
|
9
30
|
text=True,
|
|
10
|
-
timeout=
|
|
31
|
+
timeout=timeout_s,
|
|
32
|
+
env=env,
|
|
33
|
+
cwd=cwd,
|
|
11
34
|
)
|
|
12
35
|
return result.returncode, result.stdout, result.stderr
|
|
13
36
|
except subprocess.TimeoutExpired:
|
|
14
37
|
return -1, "", "TimeoutExpired: Code took too long to run."
|
|
38
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def make_sandbox(*, entry_file: str, project_root: str) -> tuple[str, str]:
|
|
8
|
+
"""Create a sandbox directory containing entry_file and discovered local imports.
|
|
9
|
+
|
|
10
|
+
Returns (sandbox_root, sandbox_entry_path).
|
|
11
|
+
|
|
12
|
+
Files are copied preserving relative paths from project_root.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from auto_code_fixer.utils import discover_all_files
|
|
16
|
+
|
|
17
|
+
project_root = os.path.abspath(project_root)
|
|
18
|
+
entry_file = os.path.abspath(entry_file)
|
|
19
|
+
|
|
20
|
+
sandbox_root = tempfile.mkdtemp(prefix="codefix_sandbox_")
|
|
21
|
+
|
|
22
|
+
files = discover_all_files(entry_file)
|
|
23
|
+
|
|
24
|
+
for src in files:
|
|
25
|
+
src = os.path.abspath(src)
|
|
26
|
+
if not src.startswith(project_root):
|
|
27
|
+
# Skip anything outside the project
|
|
28
|
+
continue
|
|
29
|
+
rel = os.path.relpath(src, project_root)
|
|
30
|
+
dst = os.path.join(sandbox_root, rel)
|
|
31
|
+
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
32
|
+
shutil.copy(src, dst)
|
|
33
|
+
|
|
34
|
+
sandbox_entry = os.path.join(sandbox_root, os.path.relpath(entry_file, project_root))
|
|
35
|
+
return sandbox_root, sandbox_entry
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def apply_sandbox_back(*, sandbox_root: str, project_root: str, changed_paths: list[str]) -> None:
|
|
39
|
+
"""Copy changed sandbox files back into project_root."""
|
|
40
|
+
|
|
41
|
+
project_root = os.path.abspath(project_root)
|
|
42
|
+
for p in changed_paths:
|
|
43
|
+
p = os.path.abspath(p)
|
|
44
|
+
if not p.startswith(os.path.abspath(sandbox_root)):
|
|
45
|
+
continue
|
|
46
|
+
rel = os.path.relpath(p, sandbox_root)
|
|
47
|
+
dst = os.path.join(project_root, rel)
|
|
48
|
+
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
49
|
+
shutil.copy(p, dst)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
_FILE_RE = re.compile(r"File \"([^\"]+)\", line (\d+)")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def pick_relevant_file(stderr: str, *, sandbox_root: str) -> str | None:
|
|
8
|
+
"""Pick the most relevant python file from a traceback.
|
|
9
|
+
|
|
10
|
+
Strategy: return the last file path that lives inside sandbox_root.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
if not stderr:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
sandbox_root = os.path.abspath(sandbox_root)
|
|
17
|
+
matches = _FILE_RE.findall(stderr)
|
|
18
|
+
if not matches:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
chosen = None
|
|
22
|
+
for path, _lineno in matches:
|
|
23
|
+
ap = os.path.abspath(path)
|
|
24
|
+
if ap.startswith(sandbox_root):
|
|
25
|
+
chosen = ap
|
|
26
|
+
|
|
27
|
+
return chosen
|
auto_code_fixer/utils.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import ast
|
|
1
2
|
import os
|
|
2
3
|
import re
|
|
3
4
|
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
VERBOSE = False
|
|
@@ -19,26 +21,73 @@ def log(message, level="INFO"):
|
|
|
19
21
|
print(f"[{timestamp}] {level}: {message}")
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
def _module_to_candidate_paths(project_root: str, module: str) -> list[str]:
|
|
25
|
+
"""Resolve a module name to potential local file paths."""
|
|
26
|
+
base = Path(project_root)
|
|
27
|
+
parts = module.split(".")
|
|
28
|
+
|
|
29
|
+
# module.py
|
|
30
|
+
p1 = base.joinpath(*parts).with_suffix(".py")
|
|
31
|
+
# module/__init__.py
|
|
32
|
+
p2 = base.joinpath(*parts, "__init__.py")
|
|
33
|
+
return [str(p1), str(p2)]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def find_imports(file_path: str, project_root: str) -> list[str]:
|
|
37
|
+
"""AST-based import discovery for local files.
|
|
38
|
+
|
|
39
|
+
This is more accurate than regex scanning and supports:
|
|
40
|
+
- import x
|
|
41
|
+
- import x.y
|
|
42
|
+
- from x import y
|
|
43
|
+
- from x.y import z
|
|
44
|
+
|
|
45
|
+
Only resolves modules that map to a file under project_root.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
found: set[str] = set()
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
src = Path(file_path).read_text(encoding="utf-8")
|
|
52
|
+
tree = ast.parse(src)
|
|
53
|
+
except Exception:
|
|
54
|
+
# Fallback to regex if parsing fails
|
|
55
|
+
import_pattern = re.compile(r"^\s*(?:from|import)\s+([a-zA-Z0-9_\.]+)")
|
|
56
|
+
for line in src.splitlines() if 'src' in locals() else Path(file_path).read_text(encoding='utf-8').splitlines():
|
|
57
|
+
m = import_pattern.match(line)
|
|
58
|
+
if not m:
|
|
59
|
+
continue
|
|
60
|
+
mod = m.group(1)
|
|
61
|
+
for c in _module_to_candidate_paths(project_root, mod):
|
|
62
|
+
if os.path.isfile(c):
|
|
63
|
+
found.add(os.path.abspath(c))
|
|
64
|
+
return sorted(found)
|
|
65
|
+
|
|
66
|
+
for node in ast.walk(tree):
|
|
67
|
+
if isinstance(node, ast.Import):
|
|
68
|
+
for alias in node.names:
|
|
69
|
+
mod = alias.name
|
|
70
|
+
for c in _module_to_candidate_paths(project_root, mod):
|
|
71
|
+
if os.path.isfile(c):
|
|
72
|
+
found.add(os.path.abspath(c))
|
|
73
|
+
|
|
74
|
+
if isinstance(node, ast.ImportFrom):
|
|
75
|
+
if not node.module:
|
|
76
|
+
continue
|
|
77
|
+
mod = node.module
|
|
78
|
+
for c in _module_to_candidate_paths(project_root, mod):
|
|
79
|
+
if os.path.isfile(c):
|
|
80
|
+
found.add(os.path.abspath(c))
|
|
81
|
+
|
|
82
|
+
return sorted(found)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def discover_all_files(entry_file: str) -> list[str]:
|
|
86
|
+
"""Discover all local python files imported from entry_file."""
|
|
25
87
|
|
|
26
|
-
with open(file_path) as f:
|
|
27
|
-
for line in f:
|
|
28
|
-
match = import_pattern.match(line)
|
|
29
|
-
if match:
|
|
30
|
-
module = match.group(1).split('.')[0]
|
|
31
|
-
candidate = os.path.join(project_root, module + ".py")
|
|
32
|
-
if os.path.isfile(candidate):
|
|
33
|
-
found.add(os.path.abspath(candidate))
|
|
34
|
-
|
|
35
|
-
return list(found)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def discover_all_files(entry_file):
|
|
39
88
|
project_root = os.path.dirname(os.path.abspath(entry_file))
|
|
40
89
|
stack = [os.path.abspath(entry_file)]
|
|
41
|
-
discovered = set()
|
|
90
|
+
discovered: set[str] = set()
|
|
42
91
|
|
|
43
92
|
while stack:
|
|
44
93
|
current = stack.pop()
|
|
@@ -50,8 +99,8 @@ def discover_all_files(entry_file):
|
|
|
50
99
|
if f not in discovered:
|
|
51
100
|
stack.append(f)
|
|
52
101
|
|
|
53
|
-
return
|
|
102
|
+
return sorted(discovered)
|
|
54
103
|
|
|
55
104
|
|
|
56
|
-
def is_in_project(file_path, project_root):
|
|
105
|
+
def is_in_project(file_path: str, project_root: str) -> bool:
|
|
57
106
|
return os.path.abspath(file_path).startswith(os.path.abspath(project_root))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_venv(dir_path: str) -> str:
|
|
8
|
+
"""Create a venv and return path to its python executable."""
|
|
9
|
+
venv_dir = Path(dir_path) / ".venv"
|
|
10
|
+
if not venv_dir.exists():
|
|
11
|
+
subprocess.check_call([sys.executable, "-m", "venv", str(venv_dir)])
|
|
12
|
+
|
|
13
|
+
# Linux/mac
|
|
14
|
+
py = venv_dir / "bin" / "python"
|
|
15
|
+
if py.exists():
|
|
16
|
+
# ensure pip exists
|
|
17
|
+
subprocess.check_call([str(py), "-m", "pip", "install", "-U", "pip"], stdout=subprocess.DEVNULL)
|
|
18
|
+
return str(py)
|
|
19
|
+
|
|
20
|
+
# Windows fallback
|
|
21
|
+
py = venv_dir / "Scripts" / "python.exe"
|
|
22
|
+
if py.exists():
|
|
23
|
+
subprocess.check_call([str(py), "-m", "pip", "install", "-U", "pip"], stdout=subprocess.DEVNULL)
|
|
24
|
+
return str(py)
|
|
25
|
+
|
|
26
|
+
raise RuntimeError(f"Could not locate venv python in {venv_dir}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def install_editable(project_root: str, *, python_exe: str) -> None:
|
|
30
|
+
"""Install project (editable) into venv for proper imports."""
|
|
31
|
+
# Avoid user site-packages contamination
|
|
32
|
+
env = {**os.environ, "PIP_DISABLE_PIP_VERSION_CHECK": "1"}
|
|
33
|
+
subprocess.check_call([python_exe, "-m", "pip", "install", "-e", project_root], env=env, stdout=subprocess.DEVNULL)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auto-code-fixer
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Automatically fix Python code using ChatGPT
|
|
5
|
+
Author-email: Arif Shah <ashah7775@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/auto-code-fixer/
|
|
8
|
+
Project-URL: Source, https://bitbucket.org/arif_automation/auto_code_fixer
|
|
9
|
+
Project-URL: Issues, https://bitbucket.org/arif_automation/auto_code_fixer/issues
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: openai>=1.0.0
|
|
19
|
+
Requires-Dist: python-decouple
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Auto Code Fixer
|
|
23
|
+
|
|
24
|
+
Auto Code Fixer is a CLI tool that **detects runtime failures** and automatically fixes Python code using OpenAI.
|
|
25
|
+
|
|
26
|
+
It is designed for real projects where an entry script imports multiple local modules. The tool runs your code in an **isolated sandbox + venv**, installs missing external dependencies, and applies fixes only after the code executes successfully.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install auto-code-fixer
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
auto-code-fixer path/to/main.py --project-root . --ask
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If you want it to overwrite without asking:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
auto-code-fixer path/to/main.py --project-root . --no-ask
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## What it fixes (core behavior)
|
|
53
|
+
|
|
54
|
+
### ✅ Local/internal imports are treated as project code
|
|
55
|
+
If your entry file imports something like:
|
|
56
|
+
|
|
57
|
+
```py
|
|
58
|
+
from mylib import add
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
…and `mylib.py` exists inside the project, Auto Code Fixer will:
|
|
62
|
+
- copy `main.py` + `mylib.py` into a sandbox
|
|
63
|
+
- execute inside the sandbox
|
|
64
|
+
- if the traceback points to `mylib.py`, it will fix `mylib.py`
|
|
65
|
+
- then apply the fix back to your repo (with backups)
|
|
66
|
+
|
|
67
|
+
### ✅ External imports are auto-installed
|
|
68
|
+
If execution fails with:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
ModuleNotFoundError: No module named 'requests'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
…it will run:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install requests
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
…but only inside the sandbox venv (so your system env isn’t polluted).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Safety
|
|
85
|
+
|
|
86
|
+
### Backups
|
|
87
|
+
Before overwriting any file, it creates a backup:
|
|
88
|
+
- `file.py.bak` (or `.bak1`, `.bak2`, ...)
|
|
89
|
+
|
|
90
|
+
### Dry run
|
|
91
|
+
```bash
|
|
92
|
+
auto-code-fixer path/to/main.py --project-root . --dry-run
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Advanced options
|
|
98
|
+
|
|
99
|
+
### Run a custom command (pytest, etc.)
|
|
100
|
+
Instead of `python main.py`, run tests:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
auto-code-fixer . --project-root . --run "pytest -q" --no-ask
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Model selection
|
|
107
|
+
```bash
|
|
108
|
+
export AUTO_CODE_FIXER_MODEL=gpt-4.1-mini
|
|
109
|
+
# or
|
|
110
|
+
auto-code-fixer main.py --model gpt-4.1-mini
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Max retries / timeout
|
|
114
|
+
```bash
|
|
115
|
+
auto-code-fixer main.py --max-retries 8 --timeout 30
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Optional AI planning (which file to edit)
|
|
119
|
+
```bash
|
|
120
|
+
auto-code-fixer main.py --ai-plan
|
|
121
|
+
```
|
|
122
|
+
This enables a helper that can suggest which local file to edit. It is best-effort.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Environment variables
|
|
127
|
+
|
|
128
|
+
- `OPENAI_API_KEY` (required unless you always pass `--api-key`)
|
|
129
|
+
- `AUTO_CODE_FIXER_MODEL` (default model)
|
|
130
|
+
- `AUTO_CODE_FIXER_ASK=true|false`
|
|
131
|
+
- `AUTO_CODE_FIXER_VERBOSE=true|false`
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Notes
|
|
136
|
+
|
|
137
|
+
- This tool edits code. Use it on a git repo so you can review diffs.
|
|
138
|
+
- For maximum safety, run with `--ask` and/or `--dry-run`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
auto_code_fixer/__init__.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
|
|
2
|
+
auto_code_fixer/cli.py,sha256=tQeFO9ou5JVqM6twFYmG2WQGqI_6ohFRb2qarUpQY1A,8735
|
|
3
|
+
auto_code_fixer/command_runner.py,sha256=6P8hGRavN5C39x-e03p02Vc805NnZH9U7e48ngb5jJI,1104
|
|
4
|
+
auto_code_fixer/fixer.py,sha256=DlAOVbw9AImEKJy8llisVCtss0tXKHMnioYeFBQGXlE,2293
|
|
5
|
+
auto_code_fixer/installer.py,sha256=x5aE-tUGTNoDUCAjtKcv-kKXsExaO7w8L7WoM8bxz9k,1358
|
|
6
|
+
auto_code_fixer/models.py,sha256=JLBJutOoiOjjlT_RMPUPhWlmm1yc_nGcQqv5tY72Al0,317
|
|
7
|
+
auto_code_fixer/patcher.py,sha256=WDYrkl12Dm3fpWppxWRszDGyD0-Sty3ud6mIZhjAMBU,686
|
|
8
|
+
auto_code_fixer/plan.py,sha256=jrZdG-f1RDxVB0tBLlTwKbCSEiOYI_RMetdzfBcyE4s,1762
|
|
9
|
+
auto_code_fixer/runner.py,sha256=BvQm3CrwkQEDOw0tpiamSTcdu3OjbOgA801xW2zWdP8,970
|
|
10
|
+
auto_code_fixer/sandbox.py,sha256=FWQcCxNDI4i7ckTKHuARSSIHCopBRqG16MVtx9s75R8,1628
|
|
11
|
+
auto_code_fixer/traceback_utils.py,sha256=caxexcBuNj19P0RG-3MtkT4XQeMcFkkgxIzzpG6SGCU,633
|
|
12
|
+
auto_code_fixer/utils.py,sha256=YXCv3PcDo5NBM1odksBTWkHTEELRtEXfPDIORA5iYaM,3090
|
|
13
|
+
auto_code_fixer/venv_manager.py,sha256=2ww8reYgLbLohh-moAD5YKM09qv_mC5yYzJRwm3XiXc,1202
|
|
14
|
+
auto_code_fixer-0.3.0.dist-info/licenses/LICENSE,sha256=hgchJNa26tjXuLztwSUDbYQxNLnAPnLk6kDXNIkC8xc,1066
|
|
15
|
+
auto_code_fixer-0.3.0.dist-info/METADATA,sha256=SUMX84ekb--wwYeNTqL0vw--yyLLRXu5njKNBX189V4,3290
|
|
16
|
+
auto_code_fixer-0.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
17
|
+
auto_code_fixer-0.3.0.dist-info/entry_points.txt,sha256=a-j2rkfwkrhXZ5Qbz_6_gwk6Bj7nijYR1DALjWp5Myk,61
|
|
18
|
+
auto_code_fixer-0.3.0.dist-info/top_level.txt,sha256=qUk1qznb6Qxqmxy2A3z_5dpOZlmNKHwUiLuJwH-CrAk,16
|
|
19
|
+
auto_code_fixer-0.3.0.dist-info/RECORD,,
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: auto-code-fixer
|
|
3
|
-
Version: 0.2.6
|
|
4
|
-
Summary: Automatically fix Python code using ChatGPT
|
|
5
|
-
Author-email: Arif Shah <ashah7775@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://pypi.org/project/auto-code-fixer/
|
|
8
|
-
Project-URL: Source, https://bitbucket.org/arif_automation/auto_code_fixer
|
|
9
|
-
Project-URL: Issues, https://bitbucket.org/arif_automation/auto_code_fixer/issues
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Requires-Python: >=3.9
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
License-File: LICENSE
|
|
18
|
-
Requires-Dist: openai>=1.0.0
|
|
19
|
-
Requires-Dist: python-decouple
|
|
20
|
-
Dynamic: license-file
|
|
21
|
-
|
|
22
|
-
# Auto Code Fixer 🛠️🤖
|
|
23
|
-
|
|
24
|
-
Auto Code Fixer is a CLI tool that automatically detects runtime errors in Python code and fixes them using ChatGPT.
|
|
25
|
-
|
|
26
|
-
It:
|
|
27
|
-
- Recursively discovers imported local Python files
|
|
28
|
-
- Runs code in an isolated temp environment
|
|
29
|
-
- Auto-installs missing dependencies
|
|
30
|
-
- Iteratively fixes errors using GPT
|
|
31
|
-
- Safely updates files only after successful execution
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## ✨ Features
|
|
36
|
-
|
|
37
|
-
- 🔍 Recursive import discovery
|
|
38
|
-
- 🧠 GPT-powered auto-fixing
|
|
39
|
-
- 📦 Auto-install missing Python libraries
|
|
40
|
-
- 🔁 Retry & fix loop
|
|
41
|
-
- 🛠 CLI-based (works from anywhere)
|
|
42
|
-
- 🔐 Safe overwrite handling
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## 📦 Installation
|
|
47
|
-
|
|
48
|
-
### ✅ Recommended (Install from PyPI)
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
pip install auto-code-fixer
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
auto_code_fixer/__init__.py,sha256=Oz5HbwHMyE87nmwV80AZzpkJPf-wBg7eDuJr_BXZkhU,22
|
|
2
|
-
auto_code_fixer/cli.py,sha256=xUVwCBTHxqxicAFqU_rbVigxP8fPL0L9m73NAUIWYy0,4147
|
|
3
|
-
auto_code_fixer/fixer.py,sha256=aoPAzUmaMSSEO4kpJ6PMWOOpWhnd7W1ikfFr9fQsHrg,1308
|
|
4
|
-
auto_code_fixer/installer.py,sha256=25rlboOI6SgJyVWDQ1NFGHukZkpIREsHa2abMYAqq7A,378
|
|
5
|
-
auto_code_fixer/runner.py,sha256=n5GR03-ZUnti0L9do0_BTsislrFzARjuP9lm_iaWsKc,388
|
|
6
|
-
auto_code_fixer/utils.py,sha256=ovWyNf3B7xMVq8qjylrPzNhrmLFaHKFF1G0vIp_GGSI,1425
|
|
7
|
-
auto_code_fixer-0.2.6.dist-info/licenses/LICENSE,sha256=hgchJNa26tjXuLztwSUDbYQxNLnAPnLk6kDXNIkC8xc,1066
|
|
8
|
-
auto_code_fixer-0.2.6.dist-info/METADATA,sha256=kGzKHtewk_p-y9gChVamngU9PScAIELAH4Edl5RNrHs,1522
|
|
9
|
-
auto_code_fixer-0.2.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
-
auto_code_fixer-0.2.6.dist-info/entry_points.txt,sha256=a-j2rkfwkrhXZ5Qbz_6_gwk6Bj7nijYR1DALjWp5Myk,61
|
|
11
|
-
auto_code_fixer-0.2.6.dist-info/top_level.txt,sha256=qUk1qznb6Qxqmxy2A3z_5dpOZlmNKHwUiLuJwH-CrAk,16
|
|
12
|
-
auto_code_fixer-0.2.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|