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.
@@ -1 +1 @@
1
- __version__ = "0.2.6"
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
- MAX_RETRIES = 5
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
- with open(file_path) as f:
25
- original_code = f.read()
25
+ project_root = os.path.abspath(project_root)
26
+ file_path = os.path.abspath(file_path)
26
27
 
27
- temp_dir = tempfile.mkdtemp(prefix="codefix_")
28
- temp_file = f"{temp_dir}/temp.py"
29
- shutil.copy(file_path, temp_file)
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
- for attempt in range(MAX_RETRIES):
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
- retcode, stdout, stderr = run_code(temp_file)
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
- if attempt > 0 and is_in_project(file_path, project_root):
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
- f"Overwrite original file '{file_path}' with fixed version? (y/n): "
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(temp_dir)
54
- return
55
-
56
- shutil.copy(temp_file, file_path)
57
- log(f"File updated: {file_path}")
58
-
59
- shutil.rmtree(temp_dir)
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
- log("Sending code & error to GPT 🧠", "DEBUG")
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(temp_file).read(),
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(temp_file).read().strip():
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(temp_file, "w") as f:
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(temp_dir)
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 ChatGPT"
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
- files = discover_all_files(args.entry_file)
152
- for file in files:
153
- fix_file(file, args.project_root, args.api_key, ask, verbose)
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 fix_code_with_gpt(original_code, error_log, api_key=None):
24
- client = get_openai_client(api_key)
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
- prompt = f"""
27
- You are a senior Python engineer.
28
- Fix the following Python code so it runs without errors.
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
- Code:
31
- {original_code}
34
+ Model can be set via:
35
+ - --model
36
+ - AUTO_CODE_FIXER_MODEL env
37
+ - default fallback
38
+ """
32
39
 
33
- Error:
34
- {error_log}
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
- Return ONLY the full corrected Python code.
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
- response = client.chat.completions.create(
40
- model="gpt-5",
41
- messages=[
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
- max_completion_tokens=1500,
69
+ temperature=0.2,
70
+ max_output_tokens=2000,
46
71
  )
47
72
 
48
- fixed_code = response.choices[0].message.content.strip()
49
-
50
- if fixed_code.startswith("```"):
51
- fixed_code = "\n".join(fixed_code.split("\n")[1:])
52
- if fixed_code.endswith("```"):
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 fixed_code.strip()
79
+ return _strip_code_fences(text)
@@ -1,16 +1,47 @@
1
1
  import re
2
2
  import subprocess
3
- import sys
4
3
 
5
4
 
6
- def check_and_install_missing_lib(stderr):
7
- match = re.search(r"No module named '([^']+)'", stderr)
8
- if not match:
9
- return False
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
- lib = match.group(1)
15
+ def install_package(package: str, *, python_exe: str) -> bool:
12
16
  try:
13
- subprocess.check_call([sys.executable, "-m", "pip", "install", lib])
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
@@ -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(path_to_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
- ["python3", path_to_code],
28
+ [exe, path_to_code],
8
29
  capture_output=True,
9
30
  text=True,
10
- timeout=15,
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 find_imports(file_path, project_root):
23
- import_pattern = re.compile(r'^\s*(?:from|import)\s+([a-zA-Z0-9_\.]+)')
24
- found = set()
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 list(discovered)
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,,