auto-code-fixer 0.2.6__tar.gz → 0.3.0__tar.gz
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-0.3.0/PKG-INFO +138 -0
- auto_code_fixer-0.3.0/README.md +117 -0
- auto_code_fixer-0.3.0/auto_code_fixer/__init__.py +1 -0
- auto_code_fixer-0.3.0/auto_code_fixer/cli.py +261 -0
- auto_code_fixer-0.3.0/auto_code_fixer/command_runner.py +45 -0
- auto_code_fixer-0.3.0/auto_code_fixer/fixer.py +79 -0
- auto_code_fixer-0.3.0/auto_code_fixer/installer.py +47 -0
- auto_code_fixer-0.3.0/auto_code_fixer/models.py +19 -0
- auto_code_fixer-0.3.0/auto_code_fixer/patcher.py +31 -0
- auto_code_fixer-0.3.0/auto_code_fixer/plan.py +61 -0
- auto_code_fixer-0.3.0/auto_code_fixer/runner.py +38 -0
- auto_code_fixer-0.3.0/auto_code_fixer/sandbox.py +49 -0
- auto_code_fixer-0.3.0/auto_code_fixer/traceback_utils.py +27 -0
- auto_code_fixer-0.3.0/auto_code_fixer/utils.py +106 -0
- auto_code_fixer-0.3.0/auto_code_fixer/venv_manager.py +33 -0
- auto_code_fixer-0.3.0/auto_code_fixer.egg-info/PKG-INFO +138 -0
- {auto_code_fixer-0.2.6 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/SOURCES.txt +10 -1
- {auto_code_fixer-0.2.6 → auto_code_fixer-0.3.0}/pyproject.toml +1 -1
- auto_code_fixer-0.3.0/tests/test_fix_imported_file.py +48 -0
- auto_code_fixer-0.3.0/tests/test_internal_imports.py +30 -0
- auto_code_fixer-0.2.6/PKG-INFO +0 -51
- auto_code_fixer-0.2.6/README.md +0 -30
- auto_code_fixer-0.2.6/auto_code_fixer/__init__.py +0 -1
- auto_code_fixer-0.2.6/auto_code_fixer/cli.py +0 -153
- auto_code_fixer-0.2.6/auto_code_fixer/fixer.py +0 -55
- auto_code_fixer-0.2.6/auto_code_fixer/installer.py +0 -16
- auto_code_fixer-0.2.6/auto_code_fixer/runner.py +0 -14
- auto_code_fixer-0.2.6/auto_code_fixer/utils.py +0 -57
- auto_code_fixer-0.2.6/auto_code_fixer.egg-info/PKG-INFO +0 -51
- {auto_code_fixer-0.2.6 → auto_code_fixer-0.3.0}/LICENSE +0 -0
- {auto_code_fixer-0.2.6 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/dependency_links.txt +0 -0
- {auto_code_fixer-0.2.6 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/entry_points.txt +0 -0
- {auto_code_fixer-0.2.6 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/requires.txt +0 -0
- {auto_code_fixer-0.2.6 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/top_level.txt +0 -0
- {auto_code_fixer-0.2.6 → auto_code_fixer-0.3.0}/setup.cfg +0 -0
|
@@ -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,117 @@
|
|
|
1
|
+
# Auto Code Fixer
|
|
2
|
+
|
|
3
|
+
Auto Code Fixer is a CLI tool that **detects runtime failures** and automatically fixes Python code using OpenAI.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install auto-code-fixer
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
auto-code-fixer path/to/main.py --project-root . --ask
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If you want it to overwrite without asking:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
auto-code-fixer path/to/main.py --project-root . --no-ask
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What it fixes (core behavior)
|
|
32
|
+
|
|
33
|
+
### ✅ Local/internal imports are treated as project code
|
|
34
|
+
If your entry file imports something like:
|
|
35
|
+
|
|
36
|
+
```py
|
|
37
|
+
from mylib import add
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
…and `mylib.py` exists inside the project, Auto Code Fixer will:
|
|
41
|
+
- copy `main.py` + `mylib.py` into a sandbox
|
|
42
|
+
- execute inside the sandbox
|
|
43
|
+
- if the traceback points to `mylib.py`, it will fix `mylib.py`
|
|
44
|
+
- then apply the fix back to your repo (with backups)
|
|
45
|
+
|
|
46
|
+
### ✅ External imports are auto-installed
|
|
47
|
+
If execution fails with:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
ModuleNotFoundError: No module named 'requests'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
…it will run:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install requests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
…but only inside the sandbox venv (so your system env isn’t polluted).
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Safety
|
|
64
|
+
|
|
65
|
+
### Backups
|
|
66
|
+
Before overwriting any file, it creates a backup:
|
|
67
|
+
- `file.py.bak` (or `.bak1`, `.bak2`, ...)
|
|
68
|
+
|
|
69
|
+
### Dry run
|
|
70
|
+
```bash
|
|
71
|
+
auto-code-fixer path/to/main.py --project-root . --dry-run
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Advanced options
|
|
77
|
+
|
|
78
|
+
### Run a custom command (pytest, etc.)
|
|
79
|
+
Instead of `python main.py`, run tests:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
auto-code-fixer . --project-root . --run "pytest -q" --no-ask
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Model selection
|
|
86
|
+
```bash
|
|
87
|
+
export AUTO_CODE_FIXER_MODEL=gpt-4.1-mini
|
|
88
|
+
# or
|
|
89
|
+
auto-code-fixer main.py --model gpt-4.1-mini
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Max retries / timeout
|
|
93
|
+
```bash
|
|
94
|
+
auto-code-fixer main.py --max-retries 8 --timeout 30
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Optional AI planning (which file to edit)
|
|
98
|
+
```bash
|
|
99
|
+
auto-code-fixer main.py --ai-plan
|
|
100
|
+
```
|
|
101
|
+
This enables a helper that can suggest which local file to edit. It is best-effort.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Environment variables
|
|
106
|
+
|
|
107
|
+
- `OPENAI_API_KEY` (required unless you always pass `--api-key`)
|
|
108
|
+
- `AUTO_CODE_FIXER_MODEL` (default model)
|
|
109
|
+
- `AUTO_CODE_FIXER_ASK=true|false`
|
|
110
|
+
- `AUTO_CODE_FIXER_VERBOSE=true|false`
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Notes
|
|
115
|
+
|
|
116
|
+
- This tool edits code. Use it on a git repo so you can review diffs.
|
|
117
|
+
- For maximum safety, run with `--ask` and/or `--dry-run`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
from decouple import config
|
|
7
|
+
|
|
8
|
+
from auto_code_fixer.runner import run_code
|
|
9
|
+
from auto_code_fixer.fixer import fix_code_with_gpt
|
|
10
|
+
from auto_code_fixer.installer import check_and_install_missing_lib
|
|
11
|
+
from auto_code_fixer.utils import (
|
|
12
|
+
discover_all_files,
|
|
13
|
+
is_in_project,
|
|
14
|
+
log,
|
|
15
|
+
set_verbose,
|
|
16
|
+
)
|
|
17
|
+
from auto_code_fixer import __version__
|
|
18
|
+
|
|
19
|
+
DEFAULT_MAX_RETRIES = 8 # can be overridden via CLI
|
|
20
|
+
|
|
21
|
+
|
|
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}")
|
|
24
|
+
|
|
25
|
+
project_root = os.path.abspath(project_root)
|
|
26
|
+
file_path = os.path.abspath(file_path)
|
|
27
|
+
|
|
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)
|
|
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):
|
|
41
|
+
log(f"Run attempt #{attempt + 1}")
|
|
42
|
+
|
|
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
|
+
)
|
|
62
|
+
|
|
63
|
+
if verbose:
|
|
64
|
+
if stdout:
|
|
65
|
+
log(f"STDOUT:\n{stdout}", "DEBUG")
|
|
66
|
+
if stderr:
|
|
67
|
+
log(f"STDERR:\n{stderr}", "DEBUG")
|
|
68
|
+
|
|
69
|
+
if retcode == 0:
|
|
70
|
+
log("Script executed successfully ✅")
|
|
71
|
+
|
|
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
|
+
|
|
76
|
+
if ask:
|
|
77
|
+
confirm = input(
|
|
78
|
+
"Overwrite original files with fixed versions?\n"
|
|
79
|
+
+ "\n".join(f"- {c}" for c in rel_changes)
|
|
80
|
+
+ "\n(y/n): "
|
|
81
|
+
).strip().lower()
|
|
82
|
+
|
|
83
|
+
if confirm != "y":
|
|
84
|
+
log("User declined overwrite", "WARN")
|
|
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)
|
|
102
|
+
log(f"Fix completed in {attempt + 1} attempt(s) 🎉")
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
log("Error detected ❌", "ERROR")
|
|
106
|
+
print(stderr)
|
|
107
|
+
|
|
108
|
+
if check_and_install_missing_lib(stderr, python_exe=venv_python, project_root=sandbox_root):
|
|
109
|
+
log("Missing dependency installed (venv), retrying…")
|
|
110
|
+
time.sleep(1)
|
|
111
|
+
continue
|
|
112
|
+
|
|
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
|
+
|
|
140
|
+
fixed_code = fix_code_with_gpt(
|
|
141
|
+
original_code=open(target_file, encoding="utf-8").read(),
|
|
142
|
+
error_log=stderr,
|
|
143
|
+
api_key=api_key,
|
|
144
|
+
model=model,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if fixed_code.strip() == open(target_file, encoding="utf-8").read().strip():
|
|
148
|
+
log("GPT returned no changes. Stopping.", "WARN")
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
with open(target_file, "w", encoding="utf-8") as f:
|
|
152
|
+
f.write(fixed_code)
|
|
153
|
+
|
|
154
|
+
changed_sandbox_files.add(os.path.abspath(target_file))
|
|
155
|
+
|
|
156
|
+
log("Code updated by GPT ✏️")
|
|
157
|
+
time.sleep(1)
|
|
158
|
+
|
|
159
|
+
log("Failed to auto-fix file after max retries ❌", "ERROR")
|
|
160
|
+
shutil.rmtree(sandbox_root)
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main():
|
|
165
|
+
parser = argparse.ArgumentParser(
|
|
166
|
+
description="Auto-fix Python code using OpenAI (advanced sandbox + retry loop)"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--version",
|
|
171
|
+
action="version",
|
|
172
|
+
version=f"%(prog)s {__version__}",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"entry_file",
|
|
177
|
+
nargs="?",
|
|
178
|
+
help="Path to the main Python file",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
parser.add_argument("--project-root", default=".")
|
|
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
|
+
)
|
|
200
|
+
|
|
201
|
+
# ✅ Proper boolean flags
|
|
202
|
+
ask_group = parser.add_mutually_exclusive_group()
|
|
203
|
+
ask_group.add_argument(
|
|
204
|
+
"--ask",
|
|
205
|
+
action="store_true",
|
|
206
|
+
help="Ask before overwriting files",
|
|
207
|
+
)
|
|
208
|
+
ask_group.add_argument(
|
|
209
|
+
"--no-ask",
|
|
210
|
+
action="store_true",
|
|
211
|
+
help="Do not ask before overwriting files",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
parser.add_argument(
|
|
215
|
+
"--verbose",
|
|
216
|
+
action="store_true",
|
|
217
|
+
help="Enable verbose/debug output",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
args = parser.parse_args()
|
|
221
|
+
|
|
222
|
+
if not args.entry_file:
|
|
223
|
+
parser.error("the following arguments are required: entry_file")
|
|
224
|
+
|
|
225
|
+
# ENV defaults
|
|
226
|
+
env_ask = config("AUTO_CODE_FIXER_ASK", default=False, cast=bool)
|
|
227
|
+
env_verbose = config("AUTO_CODE_FIXER_VERBOSE", default=False, cast=bool)
|
|
228
|
+
|
|
229
|
+
# Final ask resolution (CLI overrides ENV)
|
|
230
|
+
if args.ask:
|
|
231
|
+
ask = True
|
|
232
|
+
elif args.no_ask:
|
|
233
|
+
ask = False
|
|
234
|
+
else:
|
|
235
|
+
ask = env_ask
|
|
236
|
+
|
|
237
|
+
verbose = args.verbose or env_verbose
|
|
238
|
+
set_verbose(verbose)
|
|
239
|
+
|
|
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)."
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from decouple import config
|
|
4
|
+
from openai import OpenAI
|
|
5
|
+
|
|
6
|
+
|
|
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)
|
|
9
|
+
if not key:
|
|
10
|
+
raise RuntimeError(
|
|
11
|
+
"OpenAI API key not found. Set OPENAI_API_KEY env var or .env file or pass --api-key"
|
|
12
|
+
)
|
|
13
|
+
return OpenAI(api_key=key)
|
|
14
|
+
|
|
15
|
+
|
|
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
|
+
|
|
24
|
+
|
|
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.
|
|
33
|
+
|
|
34
|
+
Model can be set via:
|
|
35
|
+
- --model
|
|
36
|
+
- AUTO_CODE_FIXER_MODEL env
|
|
37
|
+
- default fallback
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
client = get_openai_client(api_key)
|
|
41
|
+
model = model or os.getenv("AUTO_CODE_FIXER_MODEL") or "gpt-4.1-mini"
|
|
42
|
+
|
|
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
|
+
}
|
|
51
|
+
|
|
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=[
|
|
66
|
+
{"role": "system", "content": "You fix broken Python code."},
|
|
67
|
+
{"role": "user", "content": prompt},
|
|
68
|
+
],
|
|
69
|
+
temperature=0.2,
|
|
70
|
+
max_output_tokens=2000,
|
|
71
|
+
)
|
|
72
|
+
|
|
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 ""
|
|
78
|
+
|
|
79
|
+
return _strip_code_fences(text)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
|
|
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
|
+
|
|
14
|
+
|
|
15
|
+
def install_package(package: str, *, python_exe: str) -> bool:
|
|
16
|
+
try:
|
|
17
|
+
subprocess.check_call([python_exe, "-m", "pip", "install", package])
|
|
18
|
+
return True
|
|
19
|
+
except subprocess.CalledProcessError:
|
|
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
|