gptdiff 0.1.24__py3-none-any.whl → 0.1.28__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.
gptdiff/applydiff.py CHANGED
@@ -189,6 +189,7 @@ def parse_diff_per_file(diff_text):
189
189
 
190
190
  Note:
191
191
  Uses 'b/' prefix detection from git diffs to determine target paths
192
+ This doesn't work all the time and needs to be revised with stronger models
192
193
  """
193
194
  header_re = re.compile(r'^(?:diff --git\s+)?(a/[^ ]+)\s+(b/[^ ]+)\s*$', re.MULTILINE)
194
195
  lines = diff_text.splitlines()
gptdiff/gptdiff.py CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  from pathlib import Path
3
+ from urllib.parse import urlparse
3
4
  import subprocess
4
5
  import hashlib
5
6
  import re
@@ -14,6 +15,7 @@ import pkgutil
14
15
  import contextvars
15
16
  from pkgutil import get_data
16
17
  import threading
18
+ from threading import Lock
17
19
 
18
20
  import openai
19
21
  from openai import OpenAI
@@ -203,13 +205,26 @@ def load_prepend_file(file):
203
205
  with open(file, 'r') as f:
204
206
  return f.read()
205
207
 
206
- # Function to call GPT-4 API and calculate the cost
208
+ def domain_for_url(base_url):
209
+ parsed = urlparse(base_url)
210
+ if parsed.netloc:
211
+ if parsed.username:
212
+ domain = parsed.hostname
213
+ if parsed.port:
214
+ domain += f":{parsed.port}"
215
+ else:
216
+ domain = parsed.netloc
217
+ else:
218
+ domain = base_url
219
+ return domain
220
+
207
221
  def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperature=0.7, max_tokens=30000, api_key=None, base_url=None):
208
222
  enc = tiktoken.get_encoding("o200k_base")
209
223
 
210
224
  # Use colors in print statements
211
225
  red = "\033[91m"
212
226
  green = "\033[92m"
227
+ blue = "\033[94m"
213
228
  reset = "\033[0m"
214
229
  start_time = time.time()
215
230
 
@@ -222,10 +237,13 @@ def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperat
222
237
  if 'gemini' in model:
223
238
  user_prompt = system_prompt + "\n" + user_prompt
224
239
 
240
+ input_content = system_prompt + "\n" + user_prompt + "\n" + files_content
241
+ token_count = len(enc.encode(input_content))
225
242
  messages = [
226
- {"role": "system", "content": f"{green}{system_prompt}{reset}"},
243
+ {"role": "system", "content": system_prompt},
227
244
  {"role": "user", "content": user_prompt + "\n" + files_content},
228
245
  ]
246
+
229
247
  if VERBOSE:
230
248
  print(f"{green}Using {model}{reset}")
231
249
  print(f"{green}SYSTEM PROMPT{reset}")
@@ -233,7 +251,7 @@ def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperat
233
251
  print(f"{green}USER PROMPT{reset}")
234
252
  print(user_prompt, "+", len(enc.encode(files_content)), "tokens of file content")
235
253
  else:
236
- print("Generating diff...")
254
+ print(f"Generating diff using model '{green}{model}{reset}' from '{blue}{domain_for_url(base_url)}{reset}' with {token_count} input tokens...")
237
255
 
238
256
  if not api_key:
239
257
  api_key = os.getenv('GPTDIFF_LLM_API_KEY')
@@ -248,9 +266,9 @@ def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperat
248
266
  temperature=temperature)
249
267
 
250
268
  if VERBOSE:
251
- print("RESPONSE RAW-------------")
269
+ print("Debug: Raw LLM Response\n---")
252
270
  print(response.choices[0].message.content.strip())
253
- print("/RESPONSE RAW-------------")
271
+ print("---")
254
272
  else:
255
273
  print("Diff generated.")
256
274
 
@@ -265,9 +283,6 @@ def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperat
265
283
  print("-" * 40)
266
284
 
267
285
  # Now, these rates are updated to per million tokens
268
- cost_per_million_prompt_tokens = 30
269
- cost_per_million_completion_tokens = 60
270
- cost = (prompt_tokens / 1_000_000 * cost_per_million_prompt_tokens) + (completion_tokens / 1_000_000 * cost_per_million_completion_tokens)
271
286
 
272
287
  full_response = response.choices[0].message.content.strip()
273
288
  full_response, reasoning = swallow_reasoning(full_response)
@@ -279,7 +294,7 @@ def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperat
279
294
  toolbox.use(event)
280
295
  diff_response = diff_context.get()
281
296
 
282
- return full_response, diff_response, prompt_tokens, completion_tokens, total_tokens, cost
297
+ return full_response, diff_response, prompt_tokens, completion_tokens, total_tokens
283
298
 
284
299
  # New API functions
285
300
  def build_environment(files_dict):
@@ -538,14 +553,23 @@ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
538
553
  Attempt to apply a diff via smartapply: process each file concurrently using the LLM.
539
554
  """
540
555
  from pathlib import Path
556
+ start_time = time.time()
541
557
  parsed_diffs = parse_diff_per_file(diff_text)
542
558
  print("Found", len(parsed_diffs), "files in diff, processing smart apply concurrently:")
559
+ green = "\033[92m"
560
+ red = "\033[91m"
561
+ blue = "\033[94m"
562
+ reset = "\033[0m"
563
+
543
564
  if len(parsed_diffs) == 0:
544
565
  print(colorize_warning_warning("There were no entries in this diff. The LLM may have returned something invalid."))
545
566
  if args.beep:
546
567
  print("\a")
547
568
  return
548
569
  threads = []
570
+ success_files = []
571
+ failed_files = []
572
+ success_lock = Lock()
549
573
 
550
574
  def process_file(file_path, file_diff):
551
575
  full_path = Path(project_dir) / file_path
@@ -559,11 +583,14 @@ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
559
583
  print(colorize_warning_warning(f"File {file_path} not found - skipping deletion"))
560
584
  return
561
585
 
562
- try:
563
- original_content = full_path.read_text()
564
- except (UnicodeDecodeError, IOError):
565
- print(f"Skipping file {file_path} due to read error")
566
- return
586
+ original_content = ""
587
+ if full_path.exists():
588
+ try:
589
+ original_content = full_path.read_text()
590
+ except (UnicodeDecodeError, IOError) as e:
591
+ print(f"Cannot read {file_path} due to {str(e)}, treating as new file")
592
+ else:
593
+ print(f"File {file_path} does not exist, treating as new file")
567
594
 
568
595
  # Use SMARTAPPLY-specific environment variables if set, otherwise fallback.
569
596
  smart_apply_model = os.getenv("GPTDIFF_SMARTAPPLY_MODEL")
@@ -586,9 +613,7 @@ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
586
613
  else:
587
614
  base_url = os.getenv("GPTDIFF_LLM_BASE_URL", "https://nano-gpt.com/api/v1/")
588
615
 
589
- print("-" * 40)
590
- print("Running smartapply with", model, "on", file_path)
591
- print("-" * 40)
616
+ print(f"Running smartapply in parallel for '{file_path}' using model '{green}{model}{reset}' from '{blue}{domain_for_url(base_url)}{reset}'...")
592
617
  try:
593
618
  updated_content = call_llm_for_apply_with_think_tool_available(
594
619
  file_path, original_content, file_diff, model,
@@ -597,12 +622,18 @@ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
597
622
  max_tokens=args.max_tokens)
598
623
  if updated_content.strip() == "":
599
624
  print("Cowardly refusing to write empty file to", file_path, "merge failed")
625
+ with success_lock:
626
+ failed_files.append(file_path)
600
627
  return
601
628
  full_path.parent.mkdir(parents=True, exist_ok=True)
602
629
  full_path.write_text(updated_content)
603
630
  print(f"\033[1;32mSuccessful 'smartapply' update {file_path}.\033[0m")
631
+ with success_lock:
632
+ success_files.append(file_path)
604
633
  except Exception as e:
605
634
  print(f"\033[1;31mFailed to process {file_path}: {str(e)}\033[0m")
635
+ with success_lock:
636
+ failed_files.append(file_path)
606
637
 
607
638
  for file_path, file_diff in parsed_diffs:
608
639
  thread = threading.Thread(target=process_file, args=(file_path, file_diff))
@@ -610,7 +641,17 @@ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
610
641
  threads.append(thread)
611
642
  for thread in threads:
612
643
  thread.join()
613
-
644
+ elapsed = time.time() - start_time
645
+ minutes, seconds = divmod(int(elapsed), 60)
646
+ time_str = f"{minutes}m {seconds}s" if minutes else f"{seconds}s"
647
+ print(f"Smartapply successfully applied changes in {time_str}. Check the updated files to confirm.")
648
+ if failed_files:
649
+ print(f"\033[1;31mSmart apply completed in {time_str} with failures for {len(failed_files)} files:\033[0m")
650
+ for file in failed_files:
651
+ print(f" - {file}")
652
+ print("Please check the errors above for details.")
653
+ else:
654
+ print(f"\033[1;32mSmart apply completed successfully in {time_str} for all {len(success_files)} files.\033[0m")
614
655
  if args.beep:
615
656
  print("\a")
616
657
 
@@ -710,9 +751,8 @@ def main():
710
751
  with open('prompt.txt', 'w') as f:
711
752
  f.write(full_prompt)
712
753
  print(f"Total tokens: {token_count:5d}")
713
- print(f"\033[1;32mNot calling GPT-4.\033[0m") # Green color for success message
754
+ print(f"\033[1;32mWrote full prompt to prompt.txt.\033[0m")
714
755
  print('Instead, wrote full prompt to prompt.txt. Use `xclip -selection clipboard < prompt.txt` then paste into chatgpt')
715
- print(f"Total cost: ${0.0:.4f}")
716
756
  exit(0)
717
757
  else:
718
758
  # Validate API key presence before any API operations
@@ -729,7 +769,7 @@ def main():
729
769
  print("Request canceled")
730
770
  sys.exit(0)
731
771
  try:
732
- full_text, diff_text, prompt_tokens, completion_tokens, total_tokens, cost = call_llm_for_diff(system_prompt, user_prompt, files_content, args.model,
772
+ full_text, diff_text, prompt_tokens, completion_tokens, total_tokens = call_llm_for_diff(system_prompt, user_prompt, files_content, args.model,
733
773
  temperature=args.temperature,
734
774
  api_key=os.getenv('GPTDIFF_LLM_API_KEY'),
735
775
  base_url=os.getenv('GPTDIFF_LLM_BASE_URL', "https://nano-gpt.com/api/v1/"),
@@ -741,35 +781,40 @@ def main():
741
781
  prompt_tokens = 0
742
782
  completion_tokens = 0
743
783
  total_tokens = 0
744
- cost = 0
745
784
  print(f"Error in LLM response {e}")
746
785
 
747
786
  if(diff_text.strip() == ""):
748
- print(f"\033[1;33mThere was no data in this diff. The LLM may have returned something invalid.\033[0m")
749
- print("Unable to parse diff text. Full response:", full_text)
787
+ print(f"\033[1;33mWarning: No valid diff data was generated. This could be due to an unclear prompt or an invalid LLM response.\033[0m")
788
+ print("Suggested action: Refine your prompt or check the full response below for clues.")
789
+ print("Full LLM response:\n---\n" + full_text + "\n---")
750
790
  if args.beep:
751
- print("\a") # Terminal bell for completion notification
791
+ print("\a")
752
792
  return
753
793
 
754
794
  elif args.apply:
755
795
  print("\nAttempting apply with the following diff:")
756
- print("\n<diff>")
757
796
  print(color_code_diff(diff_text))
758
- print("\n</diff>")
759
- print("Saved to patch.diff")
760
- if apply_diff(project_dir, diff_text):
761
- print(f"\033[1;32mPatch applied successfully with 'git apply'.\033[0m")
797
+ print("\033[94m**Attempting to apply patch using basic method...**\033[0m")
798
+ apply_result = apply_diff(project_dir, diff_text)
799
+ if apply_result:
800
+ print(f"\033[1;32mPatch applied successfully with basic apply.\033[0m")
762
801
  else:
763
- print("Apply failed, attempting smart apply.")
802
+ print("\033[94m**Attempting smart apply with LLM...**\033[0m")
764
803
  smart_apply_patch(project_dir, diff_text, user_prompt, args)
765
804
 
766
805
  if args.beep:
767
- print("\a") # Terminal bell for completion notification
806
+ print("\a")
768
807
 
769
- print(f"Prompt tokens: {prompt_tokens}")
770
- print(f"Completion tokens: {completion_tokens}")
771
- print(f"Total tokens: {total_tokens}")
772
- #print(f"Total cost: ${cost:.4f}")
808
+ green = "\033[92m"
809
+ reset = "\033[0m"
810
+ if VERBOSE:
811
+ print("API Usage Details:")
812
+ print(f"- Prompt tokens: {prompt_tokens}")
813
+ print(f"- Completion tokens: {completion_tokens}")
814
+ print(f"- Total tokens: {total_tokens}")
815
+ print(f"- Model used: {green}{args.model}{reset}")
816
+ else:
817
+ print(f"API Usage: {total_tokens} tokens, Model used: {green}{args.model}{reset}")
773
818
 
774
819
  def swallow_reasoning(full_response: str) -> (str, str):
775
820
  """
gptdiff/gptpatch.py CHANGED
@@ -13,7 +13,7 @@ This tool uses the same patch-application logic as gptdiff.
13
13
  import sys
14
14
  import argparse
15
15
  from pathlib import Path
16
- from gptdiff.gptdiff import apply_diff, smart_apply_patch
16
+ from gptdiff.gptdiff import apply_diff, smart_apply_patch, color_code_diff
17
17
 
18
18
  def parse_arguments():
19
19
  parser = argparse.ArgumentParser(
@@ -49,11 +49,14 @@ def parse_arguments():
49
49
  default=30000,
50
50
  help="Maximum tokens to use for LLM responses"
51
51
  )
52
+ parser.add_argument('--verbose', action='store_true', help='Enable verbose output with detailed information')
52
53
  parser.add_argument('--dumb', action='store_true', default=False, help='Attempt dumb apply before trying smart apply')
53
54
  return parser.parse_args()
54
55
 
55
56
  def main():
56
57
  args = parse_arguments()
58
+ import gptdiff.gptdiff as gd
59
+ gd.VERBOSE = args.verbose
57
60
  if args.diff:
58
61
  diff_text = args.diff
59
62
  else:
@@ -64,15 +67,21 @@ def main():
64
67
  diff_text = diff_path.read_text(encoding="utf8")
65
68
 
66
69
  project_dir = args.project_dir
70
+
71
+ if args.verbose:
72
+ print("\n\033[1;34mDiff to be applied:\033[0m")
73
+ print(color_code_diff(diff_text))
74
+ print("")
75
+
67
76
  if args.dumb:
68
77
  success = apply_diff(project_dir, diff_text)
69
78
  if success:
70
- print("✅ Diff applied successfully.")
79
+ print("\033[1;32m✅ Diff applied successfully.\033[0m")
71
80
  else:
72
- print("❌ Failed to apply diff using git apply. Attempting smart apply.")
81
+ print("\033[1;31m❌ Failed to apply diff using git apply. Attempting smart apply.\033[0m")
73
82
  smart_apply_patch(project_dir, diff_text, "", args)
74
83
  else:
75
84
  smart_apply_patch(project_dir, diff_text, "", args)
76
85
 
77
86
  if __name__ == "__main__":
78
- main()
87
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.24
3
+ Version: 0.1.28
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
@@ -82,13 +82,15 @@ done
82
82
 
83
83
  *Requires reasoning model*
84
84
 
85
- ### Why GPTDiff?
85
+ ## Why Choose GPTDiff?
86
86
 
87
- - **Understands Your Code** - Describe changes in plain English
88
- - **Safe Modifications** - Keeps existing code working
89
- - **Auto-Fix** - `--apply` fixes mistakes in generated changes
90
- - **Works Instantly** - No complex setup needed
91
- - **Whole Project View** - Handles multiple files together
87
+ - **Describe changes in plain English**
88
+ - **AI gets your whole project**
89
+ - **Auto-fixes conflicts**
90
+ - **Keeps code functional**
91
+ - **Fast setup, no fuss**
92
+ - **You approve every change**
93
+ - **Costs are upfront**
92
94
 
93
95
  ## Core Capabilities
94
96
 
@@ -0,0 +1,10 @@
1
+ gptdiff/__init__.py,sha256=o1hrK4GFvbfKcHPlLVArz4OunE3euIicEBYaLrdDo0k,198
2
+ gptdiff/applydiff.py,sha256=_11ITFMcigwvVptaIpEtyfLUTIy_mYPWExcXUqCBfOs,11200
3
+ gptdiff/gptdiff.py,sha256=Lf3utjFM0T-39YyfMXUHZmVzukJk1DxR9941KNcYyIs,33373
4
+ gptdiff/gptpatch.py,sha256=Vqk2vliYs_BxtuTpwdS88n3A8XToh6RvrCA4N8VqOu0,2759
5
+ gptdiff-0.1.28.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
6
+ gptdiff-0.1.28.dist-info/METADATA,sha256=iqKGOSe6vN62loiZ6vA2UZG-fAE80I_ZxHjMHs8CC_E,8723
7
+ gptdiff-0.1.28.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
8
+ gptdiff-0.1.28.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
9
+ gptdiff-0.1.28.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
10
+ gptdiff-0.1.28.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- gptdiff/__init__.py,sha256=o1hrK4GFvbfKcHPlLVArz4OunE3euIicEBYaLrdDo0k,198
2
- gptdiff/applydiff.py,sha256=nvTerBtFuXWf1j6nisGY7CQ6qJCIM8J9UHxgoiWReoY,11116
3
- gptdiff/gptdiff.py,sha256=XN05Zbr1H69_iG8Bx8RQ34vTXXg3WHDANRcGo3ihrhA,31518
4
- gptdiff/gptpatch.py,sha256=opakY6j_I05ZNx2ACYgxB8SxoZ3POf9iFxDkV5Yn1oU,2393
5
- gptdiff-0.1.24.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
6
- gptdiff-0.1.24.dist-info/METADATA,sha256=TE_nbtNX0IMjDek5MHxvDDUaAeVhCXw7p5kCmh0TpZg,8785
7
- gptdiff-0.1.24.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
8
- gptdiff-0.1.24.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
9
- gptdiff-0.1.24.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
10
- gptdiff-0.1.24.dist-info/RECORD,,