gptdiff 0.1.21__tar.gz → 0.1.22__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.21
3
+ Version: 0.1.22
4
4
  Summary: A tool to generate and apply git diffs using LLMs
5
5
  Author: 255labs
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -345,7 +345,8 @@ def smartapply(diff_text, files, model=None, api_key=None, base_url=None):
345
345
  del files[path]
346
346
  else:
347
347
  updated = call_llm_for_apply_with_think_tool_available(path, original, patch, model, api_key=api_key, base_url=base_url)
348
- files[path] = updated.strip()
348
+ cleaned = strip_bad_output(updated, original)
349
+ files[path] = cleaned
349
350
 
350
351
  threads = []
351
352
 
@@ -585,7 +586,7 @@ def parse_diff_per_file(diff_text):
585
586
  for line in lines:
586
587
  if header_line_re.match(line):
587
588
  if current_file is not None and current_lines:
588
- if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
589
+ if deletion_mode and not any(l.startswith("+++ /dev/null") for l in current_lines):
589
590
  current_lines.append("+++ /dev/null")
590
591
  diffs.append((current_file, "\n".join(current_lines)))
591
592
  current_lines = [line]
@@ -778,24 +779,41 @@ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
778
779
  else:
779
780
  print(f"\033[1;33mFile {file_path} not found - skipping deletion\033[0m")
780
781
  return
781
- original_content = ''
782
- if full_path.exists():
783
- try:
784
- original_content = full_path.read_text()
785
- except UnicodeDecodeError:
786
- print(f"Skipping binary file {file_path}")
787
- return
788
- if not hasattr(args, "applymodel") or args.applymodel is None:
789
- args.applymodel = args.model
790
- if args.applymodel is None:
791
- args.applymodel = os.getenv("GPTDIFF_MODEL")
782
+
783
+ try:
784
+ original_content = full_path.read_text()
785
+ except (UnicodeDecodeError, IOError):
786
+ print(f"Skipping file {file_path} due to read error")
787
+ return
788
+
789
+ # Use SMARTAPPLY-specific environment variables if set, otherwise fallback.
790
+ smart_apply_model = os.getenv("GPTDIFF_SMARTAPPLY_MODEL")
791
+ if smart_apply_model and smart_apply_model.strip():
792
+ model = smart_apply_model
793
+ elif hasattr(args, "applymodel") and args.applymodel:
794
+ model = args.applymodel
795
+ else:
796
+ model = os.getenv("GPTDIFF_MODEL", "deepseek-reasoner")
797
+
798
+ smart_api_key = os.getenv("GPTDIFF_SMARTAPPLY_API_KEY")
799
+ if smart_api_key and smart_api_key.strip():
800
+ api_key = smart_api_key
801
+ else:
802
+ api_key = os.getenv("GPTDIFF_LLM_API_KEY")
803
+
804
+ smart_base_url = os.getenv("GPTDIFF_SMARTAPPLY_BASE_URL")
805
+ if smart_base_url and smart_base_url.strip():
806
+ base_url = smart_base_url
807
+ else:
808
+ base_url = os.getenv("GPTDIFF_LLM_BASE_URL", "https://nano-gpt.com/api/v1/")
792
809
 
793
810
  print("-" * 40)
794
- print("Running smartapply with", args.applymodel,"on",file_path)
811
+ print("Running smartapply with", model, "on", file_path)
795
812
  print("-" * 40)
796
813
  try:
797
814
  updated_content = call_llm_for_apply_with_think_tool_available(
798
- file_path, original_content, file_diff, args.applymodel,
815
+ file_path, original_content, file_diff, model,
816
+ api_key=api_key, base_url=base_url,
799
817
  extra_prompt=f"This changeset is from the following instructions:\n{user_prompt}",
800
818
  max_tokens=args.max_tokens)
801
819
  if updated_content.strip() == "":
@@ -1003,5 +1021,29 @@ def swallow_reasoning(full_response: str) -> (str, str):
1003
1021
  final_content = full_response.strip()
1004
1022
  return final_content, reasoning
1005
1023
 
1024
+ def strip_bad_output(updated: str, original: str) -> str:
1025
+ """
1026
+ If the original file content does not start with a code fence but the LLM’s updated output
1027
+ starts with triple backticks (possibly with an introductory message), extract and return only
1028
+ the content within the first code block.
1029
+ """
1030
+ updated_stripped = updated.strip()
1031
+ # If the original file does not start with a code fence, but the updated output contains a code block,
1032
+ # extract and return only the content inside the first code block.
1033
+ if not original.lstrip().startswith("```"):
1034
+ # Search for the first code block in the updated output.
1035
+ m = re.search(r"```(.*?)```", updated_stripped, re.DOTALL)
1036
+ if m:
1037
+ content = m.group(1).strip()
1038
+ lines = content.splitlines()
1039
+ if len(lines) > 1:
1040
+ first_line = lines[0].strip()
1041
+ # If the first line appears to be a language specifier (i.e., a single word)
1042
+ # and is not "diff", then drop it.
1043
+ if " " not in first_line and first_line.lower() != "diff":
1044
+ content = "\n".join(lines[1:]).strip()
1045
+ return content
1046
+ return updated_stripped
1047
+
1006
1048
  if __name__ == "__main__":
1007
- main()
1049
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.21
3
+ Version: 0.1.22
4
4
  Summary: A tool to generate and apply git diffs using LLMs
5
5
  Author: 255labs
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -16,4 +16,5 @@ tests/test_diff_parse.py
16
16
  tests/test_failing_case.py
17
17
  tests/test_parse_diff_per_file.py
18
18
  tests/test_smartapply.py
19
+ tests/test_strip_bad_ouput.py
19
20
  tests/test_swallow_reasoning.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='gptdiff',
5
- version='0.1.21',
5
+ version='0.1.22',
6
6
  description='A tool to generate and apply git diffs using LLMs',
7
7
  author='255labs',
8
8
  packages=find_packages(), # Use find_packages() to automatically discover packages
@@ -0,0 +1,90 @@
1
+ # File: tests/test_strip_bad_output.py
2
+ import pytest
3
+ from gptdiff.gptdiff import strip_bad_output
4
+
5
+ def test_strip_bad_output_removes_wrapping():
6
+ """
7
+ If the original file content does not start with a code fence,
8
+ but the LLM output starts with a code block and extra text,
9
+ then only the content inside the first code block should be returned.
10
+ """
11
+ # Original file content does not start with a code fence.
12
+ original = "def hello():\n print('Hello')\n"
13
+ # Simulated LLM output with extraneous text and a code block.
14
+ updated = (
15
+ "This is the file you requested:\n"
16
+ "```diff\n"
17
+ "def hello():\n"
18
+ " print('Goodbye')\n"
19
+ "```\n"
20
+ "Thank you!"
21
+ )
22
+ # We expect the function to extract only the content inside the code block.
23
+ expected = "diff\ndef hello():\n print('Goodbye')"
24
+ result = strip_bad_output(updated, original)
25
+ assert result == expected, f"Expected:\n{expected}\nGot:\n{result}"
26
+
27
+ def test_strip_bad_output_no_change_when_original_has_code_block():
28
+ """
29
+ If the original file already starts with a code fence,
30
+ the function should leave the updated output unchanged.
31
+ """
32
+ original = "```diff\ndef hello():\n print('Hello')\n```"
33
+ updated = "```diff\ndef hello():\n print('Modified')\n```"
34
+ expected = updated.strip()
35
+ result = strip_bad_output(updated, original)
36
+ assert result == expected, "Expected no changes when original already starts with a code fence"
37
+
38
+ def test_strip_bad_output_no_wrapping_detected():
39
+ """
40
+ If the updated output does not start with a code fence,
41
+ the function should return the updated output unchanged.
42
+ """
43
+ original = "def hello():\n print('Hello')\n"
44
+ updated = "def hello():\n print('Modified')\n"
45
+ expected = updated.strip()
46
+ result = strip_bad_output(updated, original)
47
+ assert result == expected, "Expected output to remain unchanged if no code block is detected"
48
+
49
+
50
+ def test_strip_bad_output_prod_case():
51
+ """
52
+ Test that when the updated output includes extraneous introductory text and
53
+ a language specifier in the code block, the function extracts only the content
54
+ within the code block (without the language tag or extra text).
55
+
56
+ For example, given an updated output like:
57
+
58
+ Here's the entire file after applying the diff:
59
+
60
+ ```typescript
61
+ def foo():
62
+ print('Modified')
63
+ ```
64
+ Some trailing text that should be ignored.
65
+
66
+ the expected extracted content is:
67
+
68
+ def foo():
69
+ print('Modified')
70
+ """
71
+ # Original file content does not start with a code fence.
72
+ original = "def foo():\n pass\n"
73
+
74
+ # Simulated LLM output with extraneous text, a language specifier ("typescript"),
75
+ # and trailing text.
76
+ updated = (
77
+ "Here's the entire file after applying the diff:\n\n"
78
+ "```typescript\n"
79
+ "def foo():\n"
80
+ " print('Modified')\n"
81
+ "```\n"
82
+ "Some trailing text that should be ignored."
83
+ )
84
+
85
+ # We expect the function to extract only the content inside the first code block,
86
+ # ignoring the language specifier and any text outside the code block.
87
+ expected = "def foo():\n print('Modified')"
88
+
89
+ result = strip_bad_output(updated, original)
90
+ assert result == expected, f"Expected:\n{expected}\nGot:\n{result}"
File without changes
File without changes
File without changes
File without changes
File without changes