auto-code-fixer 0.3.5__py3-none-any.whl → 0.3.6__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.3.5"
1
+ __version__ = "0.3.6"
auto_code_fixer/cli.py CHANGED
@@ -32,6 +32,9 @@ def fix_file(
32
32
  max_retries: int,
33
33
  run_cmd: str | None,
34
34
  patch_protocol: bool,
35
+ max_files_changed: int,
36
+ context_files: int,
37
+ approve: bool,
35
38
  fmt: str | None,
36
39
  lint: str | None,
37
40
  lint_fix: bool,
@@ -247,33 +250,99 @@ def fix_file(
247
250
 
248
251
  if patch_protocol:
249
252
  try:
253
+ import difflib
254
+
250
255
  from auto_code_fixer.fixer import fix_code_with_gpt_patch_protocol
251
256
  from auto_code_fixer.patch_protocol import (
252
257
  parse_patch_protocol_response,
253
258
  validate_and_resolve_patch_files,
254
259
  )
255
- from auto_code_fixer.patcher import safe_read, safe_write
260
+ from auto_code_fixer.patcher import safe_read, atomic_write_verified_sha256
261
+ from auto_code_fixer.utils import find_imports
256
262
 
257
263
  hint_paths = [os.path.relpath(target_file, sandbox_root)]
264
+
265
+ # Add a few related local files as read-only context to reduce back-and-forth.
266
+ ctx_pairs: list[tuple[str, str]] = []
267
+ if context_files and context_files > 0:
268
+ rels: list[str] = []
269
+ for abs_p in find_imports(target_file, sandbox_root):
270
+ try:
271
+ rels.append(os.path.relpath(abs_p, sandbox_root))
272
+ except Exception:
273
+ continue
274
+ # drop duplicates and the target itself
275
+ rels = [r for r in rels if r not in hint_paths]
276
+ for rel in rels[:context_files]:
277
+ abs_p = os.path.join(sandbox_root, rel)
278
+ if os.path.exists(abs_p):
279
+ try:
280
+ ctx_pairs.append((rel, safe_read(abs_p)))
281
+ except Exception:
282
+ pass
283
+
258
284
  raw = fix_code_with_gpt_patch_protocol(
259
285
  sandbox_root=sandbox_root,
260
286
  error_log=stderr,
261
287
  api_key=api_key,
262
288
  model=model,
263
289
  hint_paths=hint_paths,
290
+ context_files=ctx_pairs,
264
291
  )
265
292
 
266
293
  patch_files = parse_patch_protocol_response(raw)
294
+
295
+ if len(patch_files) > max_files_changed:
296
+ raise ValueError(
297
+ f"Patch wants to change {len(patch_files)} files, exceeding --max-files-changed={max_files_changed}"
298
+ )
299
+
267
300
  resolved = validate_and_resolve_patch_files(patch_files, sandbox_root=sandbox_root)
268
301
 
269
- # Apply patches (only if they change content)
302
+ # Prepare diffs / approvals and apply atomically.
303
+ sha_by_rel = {pf.path: pf.sha256 for pf in patch_files}
304
+
305
+ planned: list[tuple[str, str, str, str]] = [] # (abs_path, rel, old, new)
270
306
  for abs_path, new_content in resolved:
271
307
  old = ""
272
308
  if os.path.exists(abs_path):
273
309
  old = safe_read(abs_path)
274
310
  if new_content.strip() == (old or "").strip():
275
311
  continue
276
- safe_write(abs_path, new_content)
312
+ rel = os.path.relpath(abs_path, sandbox_root)
313
+ planned.append((abs_path, rel, old, new_content))
314
+
315
+ if planned:
316
+ if len(planned) > max_files_changed:
317
+ raise ValueError(
318
+ f"Patch would change {len(planned)} files, exceeding --max-files-changed={max_files_changed}"
319
+ )
320
+
321
+ if approve:
322
+ print("\nPROPOSED CHANGES (patch protocol):")
323
+ for _, rel, old, new in planned:
324
+ diff = "".join(
325
+ difflib.unified_diff(
326
+ (old or "").splitlines(keepends=True),
327
+ (new or "").splitlines(keepends=True),
328
+ fromfile=rel + " (before)",
329
+ tofile=rel + " (after)",
330
+ )
331
+ )
332
+ print(diff)
333
+
334
+ confirm = input("Apply these changes in the sandbox? (y/n): ").strip().lower()
335
+ if confirm != "y":
336
+ log("User declined patch application", "WARN")
337
+ planned = []
338
+
339
+ for abs_path, rel, old, new_content in planned:
340
+ expected_sha = sha_by_rel.get(rel)
341
+ if not expected_sha:
342
+ # Shouldn't happen; keep safe.
343
+ raise ValueError(f"Missing sha256 for {rel} in patch protocol payload")
344
+
345
+ atomic_write_verified_sha256(abs_path, new_content, expected_sha)
277
346
  changed_sandbox_files.add(os.path.abspath(abs_path))
278
347
  applied_any = True
279
348
 
@@ -292,9 +361,29 @@ def fix_file(
292
361
  log("GPT returned no changes. Stopping.", "WARN")
293
362
  break
294
363
 
295
- with open(target_file, "w", encoding="utf-8") as f:
296
- f.write(fixed_code)
364
+ if approve:
365
+ import difflib
366
+
367
+ old = open(target_file, encoding="utf-8").read()
368
+ diff = "".join(
369
+ difflib.unified_diff(
370
+ (old or "").splitlines(keepends=True),
371
+ (fixed_code or "").splitlines(keepends=True),
372
+ fromfile=os.path.relpath(target_file, sandbox_root) + " (before)",
373
+ tofile=os.path.relpath(target_file, sandbox_root) + " (after)",
374
+ )
375
+ )
376
+ print("\nPROPOSED CHANGES (legacy mode):")
377
+ print(diff)
378
+ confirm = input("Apply this change in the sandbox? (y/n): ").strip().lower()
379
+ if confirm != "y":
380
+ log("User declined patch application", "WARN")
381
+ cleanup_sandbox()
382
+ return False
383
+
384
+ from auto_code_fixer.patcher import safe_write
297
385
 
386
+ safe_write(target_file, fixed_code)
298
387
  changed_sandbox_files.add(os.path.abspath(target_file))
299
388
  applied_any = True
300
389
 
@@ -346,14 +435,37 @@ def main():
346
435
  )
347
436
 
348
437
  parser.add_argument(
349
- "--patch-protocol",
438
+ "--legacy-mode",
350
439
  action="store_true",
351
440
  help=(
352
- "Optional: ask the model for a strict JSON patch protocol with sha256 verification "
353
- "(falls back to full-text mode if parsing/validation fails)"
441
+ "Disable the default JSON patch protocol and use legacy full-text edit mode only. "
442
+ "(Not recommended; patch protocol is safer and supports multi-file fixes.)"
354
443
  ),
355
444
  )
356
445
 
446
+ parser.add_argument(
447
+ "--max-files-changed",
448
+ type=int,
449
+ default=20,
450
+ help="Safety guard: maximum number of files the model may change per attempt (default: 20)",
451
+ )
452
+
453
+ parser.add_argument(
454
+ "--context-files",
455
+ type=int,
456
+ default=3,
457
+ help=(
458
+ "Include up to N related local files (imports) as read-only context in the LLM prompt "
459
+ "when using patch protocol (default: 3)"
460
+ ),
461
+ )
462
+
463
+ parser.add_argument(
464
+ "--approve",
465
+ action="store_true",
466
+ help="Show diffs for proposed changes and ask for approval before applying them in the sandbox",
467
+ )
468
+
357
469
  parser.add_argument(
358
470
  "--format",
359
471
  default=None,
@@ -426,7 +538,10 @@ def main():
426
538
  timeout_s=args.timeout,
427
539
  max_retries=args.max_retries,
428
540
  run_cmd=args.run,
429
- patch_protocol=args.patch_protocol,
541
+ patch_protocol=not args.legacy_mode,
542
+ max_files_changed=args.max_files_changed,
543
+ context_files=args.context_files,
544
+ approve=args.approve,
430
545
  fmt=args.format,
431
546
  lint=args.lint,
432
547
  lint_fix=args.fix,
auto_code_fixer/fixer.py CHANGED
@@ -86,12 +86,13 @@ def fix_code_with_gpt_patch_protocol(
86
86
  api_key: str | None = None,
87
87
  model: str | None = None,
88
88
  hint_paths: list[str] | None = None,
89
+ context_files: list[tuple[str, str]] | None = None,
89
90
  ) -> str:
90
91
  """Ask the model for structured edits (patch protocol).
91
92
 
92
93
  Returns raw model text (expected JSON). Parsing/validation happens elsewhere.
93
94
 
94
- The protocol is optional and should be enabled explicitly by the caller.
95
+ context_files: optional list of (relative_path, content) to include as read-only context.
95
96
  """
96
97
 
97
98
  client = get_openai_client(api_key)
@@ -113,11 +114,23 @@ def fix_code_with_gpt_patch_protocol(
113
114
  f"- {p}" for p in hint_paths
114
115
  )
115
116
 
117
+ ctx = ""
118
+ if context_files:
119
+ # Keep it readable and avoid enormous prompts.
120
+ blocks: list[str] = []
121
+ for rel, content in context_files:
122
+ snippet = content
123
+ if len(snippet) > 4000:
124
+ snippet = snippet[:4000] + "\n... (truncated)"
125
+ blocks.append(f"--- FILE: {rel} ---\n{snippet}\n")
126
+ ctx = "\n\nREAD-ONLY CONTEXT FILES:\n" + "\n".join(blocks)
127
+
116
128
  prompt = (
117
129
  "You are a senior Python engineer. Fix the project so it runs without errors.\n"
118
130
  "Return ONLY valid JSON that matches this schema (no markdown, no commentary):\n"
119
131
  + json.dumps(schema)
120
132
  + hints
133
+ + ctx
121
134
  + "\n\nSANDBOX ROOT:\n"
122
135
  + sandbox_root
123
136
  + "\n\nERROR LOG:\n"
@@ -1,6 +1,9 @@
1
1
  import os
2
+ import tempfile
2
3
  from dataclasses import dataclass
3
4
 
5
+ from auto_code_fixer.patch_protocol import compute_sha256_utf8
6
+
4
7
 
5
8
  @dataclass
6
9
  class FileEdit:
@@ -8,10 +11,43 @@ class FileEdit:
8
11
  new_content: str
9
12
 
10
13
 
14
+ def _atomic_write(path: str, content: str) -> None:
15
+ """Atomically write text content to path.
16
+
17
+ Writes to a temp file in the same directory then os.replace() to avoid partial writes.
18
+ """
19
+
20
+ dir_name = os.path.dirname(path) or "."
21
+ os.makedirs(dir_name, exist_ok=True)
22
+
23
+ fd, tmp_path = tempfile.mkstemp(prefix=".acf_", suffix=".tmp", dir=dir_name)
24
+ try:
25
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
26
+ f.write(content)
27
+ f.flush()
28
+ os.fsync(f.fileno())
29
+ os.replace(tmp_path, path)
30
+ finally:
31
+ try:
32
+ if os.path.exists(tmp_path):
33
+ os.unlink(tmp_path)
34
+ except Exception:
35
+ pass
36
+
37
+
38
+ def atomic_write_verified_sha256(path: str, content: str, expected_sha256: str) -> None:
39
+ """Atomically write text content, verifying sha256(content) before commit."""
40
+
41
+ got = compute_sha256_utf8(content)
42
+ if got != (expected_sha256 or "").strip().lower():
43
+ raise ValueError(f"sha256 mismatch for {path}: expected {expected_sha256}, got {got}")
44
+
45
+ _atomic_write(path, content)
46
+
47
+
11
48
  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)
49
+ # Backwards-compatible name; now atomic.
50
+ _atomic_write(path, content)
15
51
 
16
52
 
17
53
  def safe_read(path: str) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auto-code-fixer
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: Automatically fix Python code using ChatGPT
5
5
  Author-email: Arif Shah <ashah7775@gmail.com>
6
6
  License: MIT
@@ -1,20 +1,20 @@
1
- auto_code_fixer/__init__.py,sha256=ThnCuF3X7rsQSd5PAea_jfYA70ZmhLvkFcLBxBPwZnY,22
2
- auto_code_fixer/cli.py,sha256=0UszYX0cmDQIQpuLYrRa_tJCklnzUiqDAAauOfsboeg,15307
1
+ auto_code_fixer/__init__.py,sha256=W_9dCm49nLvZulVAvvsafxLJjVBSKDBHz9K7szFZllo,22
2
+ auto_code_fixer/cli.py,sha256=lFIGgrMQaqAoNtGRlkKXE1Lzk4l74XwsYjdxnrlF6lI,20304
3
3
  auto_code_fixer/command_runner.py,sha256=6P8hGRavN5C39x-e03p02Vc805NnZH9U7e48ngb5jJI,1104
4
- auto_code_fixer/fixer.py,sha256=s4owenfSKpoutzXyhVJ2fVecHp12ioQf5s8uPYdWbN0,4180
4
+ auto_code_fixer/fixer.py,sha256=zcgw56pRTuOLvna09lTXatD0VWwjjzBVk0OyEKfgxDM,4691
5
5
  auto_code_fixer/installer.py,sha256=LC0jasSsPI7eHMeDxa622OoMCR1951HAXUZWp-kcmVY,1522
6
6
  auto_code_fixer/models.py,sha256=JLBJutOoiOjjlT_RMPUPhWlmm1yc_nGcQqv5tY72Al0,317
7
7
  auto_code_fixer/patch_protocol.py,sha256=8l1E9o-3jkO4VAI7Ulrf-1MbAshNzjQXtUkmH-0hYio,3216
8
- auto_code_fixer/patcher.py,sha256=WDYrkl12Dm3fpWppxWRszDGyD0-Sty3ud6mIZhjAMBU,686
8
+ auto_code_fixer/patcher.py,sha256=BcQTnjWazdpuEXyR2AlumFBzIk_yIrO3fGTaIqpHuiU,1811
9
9
  auto_code_fixer/plan.py,sha256=jrZdG-f1RDxVB0tBLlTwKbCSEiOYI_RMetdzfBcyE4s,1762
10
10
  auto_code_fixer/runner.py,sha256=BvQm3CrwkQEDOw0tpiamSTcdu3OjbOgA801xW2zWdP8,970
11
11
  auto_code_fixer/sandbox.py,sha256=FWQcCxNDI4i7ckTKHuARSSIHCopBRqG16MVtx9s75R8,1628
12
12
  auto_code_fixer/traceback_utils.py,sha256=sbSuLO-2UBk5QPJZYJunTK9WGOpEY8mxR6WRKbtCIoM,935
13
13
  auto_code_fixer/utils.py,sha256=YXCv3PcDo5NBM1odksBTWkHTEELRtEXfPDIORA5iYaM,3090
14
14
  auto_code_fixer/venv_manager.py,sha256=2ww8reYgLbLohh-moAD5YKM09qv_mC5yYzJRwm3XiXc,1202
15
- auto_code_fixer-0.3.5.dist-info/licenses/LICENSE,sha256=hgchJNa26tjXuLztwSUDbYQxNLnAPnLk6kDXNIkC8xc,1066
16
- auto_code_fixer-0.3.5.dist-info/METADATA,sha256=e2BJl84gGASwLXcrv3B1AjeTg8dVqnQcgOAN4KRCRb8,3870
17
- auto_code_fixer-0.3.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
18
- auto_code_fixer-0.3.5.dist-info/entry_points.txt,sha256=a-j2rkfwkrhXZ5Qbz_6_gwk6Bj7nijYR1DALjWp5Myk,61
19
- auto_code_fixer-0.3.5.dist-info/top_level.txt,sha256=qUk1qznb6Qxqmxy2A3z_5dpOZlmNKHwUiLuJwH-CrAk,16
20
- auto_code_fixer-0.3.5.dist-info/RECORD,,
15
+ auto_code_fixer-0.3.6.dist-info/licenses/LICENSE,sha256=hgchJNa26tjXuLztwSUDbYQxNLnAPnLk6kDXNIkC8xc,1066
16
+ auto_code_fixer-0.3.6.dist-info/METADATA,sha256=R6geFjh2cpLvAzuR1KQRcAJyryQgClYYB1wWmD1o2Qk,3870
17
+ auto_code_fixer-0.3.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
18
+ auto_code_fixer-0.3.6.dist-info/entry_points.txt,sha256=a-j2rkfwkrhXZ5Qbz_6_gwk6Bj7nijYR1DALjWp5Myk,61
19
+ auto_code_fixer-0.3.6.dist-info/top_level.txt,sha256=qUk1qznb6Qxqmxy2A3z_5dpOZlmNKHwUiLuJwH-CrAk,16
20
+ auto_code_fixer-0.3.6.dist-info/RECORD,,