gptdiff 0.1.11__py3-none-any.whl → 0.1.14__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
gptdiff/gptdiff.py CHANGED
@@ -747,6 +747,69 @@ def build_environment_from_filelist(file_list, cwd):
747
747
  continue
748
748
  return build_environment(files_dict)
749
749
 
750
+ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
751
+ """
752
+ Attempt to apply a diff via smartapply: process each file concurrently using the LLM.
753
+ """
754
+ from pathlib import Path
755
+ parsed_diffs = parse_diff_per_file(diff_text)
756
+ print("Found", len(parsed_diffs), "files in diff, processing smart apply concurrently:")
757
+ if len(parsed_diffs) == 0:
758
+ print("\033[1;33mThere were no entries in this diff. The LLM may have returned something invalid.\033[0m")
759
+ if args.beep:
760
+ print("\a")
761
+ return
762
+ threads = []
763
+
764
+ def process_file(file_path, file_diff):
765
+ full_path = Path(project_dir) / file_path
766
+ print(f"Processing file: {file_path}")
767
+ if '+++ /dev/null' in file_diff:
768
+ if full_path.exists():
769
+ full_path.unlink()
770
+ print(f"\033[1;32mDeleted file {file_path}.\033[0m")
771
+ else:
772
+ print(f"\033[1;33mFile {file_path} not found - skipping deletion\033[0m")
773
+ return
774
+ original_content = ''
775
+ if full_path.exists():
776
+ try:
777
+ original_content = full_path.read_text()
778
+ except UnicodeDecodeError:
779
+ print(f"Skipping binary file {file_path}")
780
+ return
781
+ if not hasattr(args, "applymodel") or args.applymodel is None:
782
+ args.applymodel = args.model
783
+ if args.applymodel is None:
784
+ args.applymodel = os.getenv("GPTDIFF_MODEL")
785
+
786
+ print("-" * 40)
787
+ print("Running smartapply with", args.applymodel,"on",file_path)
788
+ print("-" * 40)
789
+ try:
790
+ updated_content = call_llm_for_apply_with_think_tool_available(
791
+ file_path, original_content, file_diff, args.applymodel,
792
+ extra_prompt=f"This changeset is from the following instructions:\n{user_prompt}",
793
+ max_tokens=args.max_tokens)
794
+ if updated_content.strip() == "":
795
+ print("Cowardly refusing to write empty file to", file_path, "merge failed")
796
+ return
797
+ full_path.parent.mkdir(parents=True, exist_ok=True)
798
+ full_path.write_text(updated_content)
799
+ print(f"\033[1;32mSuccessful 'smartapply' update {file_path}.\033[0m")
800
+ except Exception as e:
801
+ print(f"\033[1;31mFailed to process {file_path}: {str(e)}\033[0m")
802
+
803
+ for file_path, file_diff in parsed_diffs:
804
+ thread = threading.Thread(target=process_file, args=(file_path, file_diff))
805
+ thread.start()
806
+ threads.append(thread)
807
+ for thread in threads:
808
+ thread.join()
809
+
810
+ if args.beep:
811
+ print("\a")
812
+
750
813
  def main():
751
814
  # Adding color support for Windows CMD
752
815
  if os.name == 'nt':
@@ -754,7 +817,6 @@ def main():
754
817
 
755
818
  args = parse_arguments()
756
819
 
757
- # TODO: The 'openai.api_base' option isn't read in the client API. You will need to pass it when you instantiate the client, e.g. 'OpenAI(base_url="https://nano-gpt.com/api/v1/")'
758
820
  # openai.api_base = "https://nano-gpt.com/api/v1/"
759
821
  if len(sys.argv) < 2:
760
822
  print("Usage: python script.py '<user_prompt>' [--apply]")
@@ -778,11 +840,24 @@ def main():
778
840
  project_files.extend(load_project_files(additional_path, project_dir))
779
841
 
780
842
  if args.prepend:
781
- prepend = load_prepend_file(args.prepend)
782
- print("Including prepend",len(enc.encode(json.dumps(prepend))), "tokens")
843
+ prepend = args.prepend
783
844
  else:
784
845
  prepend = ""
785
846
 
847
+ if prepend.startswith("http://") or prepend.startswith("https://"):
848
+ try:
849
+ import urllib.request
850
+ with urllib.request.urlopen(prepend) as response:
851
+ prepend = response.read().decode('utf-8')
852
+ except Exception as e:
853
+ print(f"Error fetching prepend content from URL {prepend}: {e}")
854
+ prepend = ""
855
+ elif os.path.exists(prepend):
856
+ prepend = load_prepend_file(prepend)
857
+ else:
858
+ # If the specified prepend path does not exist, treat the value as literal content.
859
+ prepend = prepend
860
+
786
861
  # Prepare system prompt
787
862
  system_prompt = prepend + f"Output a git diff into a <diff> block."
788
863
 
@@ -851,73 +926,11 @@ def main():
851
926
  print("\n</diff>")
852
927
  print("Saved to patch.diff")
853
928
  if apply_diff(project_dir, diff_text):
854
- print(f"\033[1;32mPatch applied successfully with 'git apply'.\033[0m") # Green color for success message
929
+ print(f"\033[1;32mPatch applied successfully with 'git apply'.\033[0m")
855
930
  else:
856
931
  print("Apply failed, attempting smart apply.")
857
- parsed_diffs = parse_diff_per_file(diff_text)
858
- print("Found", len(parsed_diffs), " files in diff, calling smartdiff for each file concurrently:")
859
-
860
- if(len(parsed_diffs) == 0):
861
- print(f"\033[1;33mThere were no entries in this diff. The LLM may have returned something invalid.\033[0m")
862
- if args.beep:
863
- print("\a") # Terminal bell for completion notification
864
- return
865
-
866
- threads = []
867
-
868
- def process_file(file_path, file_diff):
869
- full_path = Path(project_dir) / file_path
870
- print(f"Processing file: {file_path}")
871
-
872
- # Handle file deletions from diff
873
- if '+++ /dev/null' in file_diff:
874
- if full_path.exists():
875
- full_path.unlink()
876
- print(f"\033[1;32mDeleted file {file_path}.\033[0m")
877
- else:
878
- print(f"\033[1;33mFile {file_path} not found - skipping deletion\033[0m")
879
- return
880
-
881
- original_content = ''
882
- if full_path.exists():
883
- try:
884
- original_content = full_path.read_text()
885
- except UnicodeDecodeError:
886
- print(f"Skipping binary file {file_path}")
887
- return
888
-
889
- print("-" * 40)
890
- print("SMARTAPPLY")
891
- print(file_diff)
892
- print("-" * 40)
893
- if args.applymodel is None:
894
- args.applymodel = args.model
895
-
896
- try:
897
- updated_content = call_llm_for_apply_with_think_tool_available(file_path, original_content, file_diff, args.applymodel, extra_prompt=f"This changeset is from the following instructions:\n{user_prompt}", max_tokens=args.max_tokens)
898
-
899
- if updated_content.strip() == "":
900
- print("Cowardly refusing to write empty file to", file_path, "merge failed")
901
- return
902
-
903
- full_path.parent.mkdir(parents=True, exist_ok=True)
904
- full_path.write_text(updated_content)
905
- print(f"\033[1;32mSuccessful 'smartapply' update {file_path}.\033[0m")
906
- except Exception as e:
907
- print(f"\033[1;31mFailed to process {file_path}: {str(e)}\033[0m")
908
-
909
- threads = []
910
- for file_path, file_diff in parsed_diffs:
911
- thread = threading.Thread(
912
- target=process_file,
913
- args=(file_path, file_diff)
914
- )
915
- thread.start()
916
- threads.append(thread)
917
- for thread in threads:
918
- thread.join()
932
+ smart_apply_patch(project_dir, diff_text, user_prompt, args)
919
933
 
920
-
921
934
  if args.beep:
922
935
  print("\a") # Terminal bell for completion notification
923
936
 
@@ -927,4 +940,4 @@ def main():
927
940
  #print(f"Total cost: ${cost:.4f}")
928
941
 
929
942
  if __name__ == "__main__":
930
- main()
943
+ main()
gptdiff/gptpatch.py ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Command line tool to apply a unified diff directly to a file system.
4
+
5
+ Usage:
6
+ gptpatch --diff "<diff text>"
7
+ or
8
+ gptpatch path/to/diff.patch
9
+
10
+ This tool uses the same patch-application logic as gptdiff.
11
+ """
12
+
13
+ import sys
14
+ import argparse
15
+ from pathlib import Path
16
+ from gptdiff.gptdiff import apply_diff
17
+
18
+
19
+ def parse_arguments():
20
+ parser = argparse.ArgumentParser(
21
+ description="Apply a unified diff to the file system using GPTDiff's patch logic."
22
+ )
23
+ group = parser.add_mutually_exclusive_group(required=True)
24
+ group.add_argument(
25
+ "--diff",
26
+ type=str,
27
+ help="Unified diff text to apply (provide as a string)."
28
+ )
29
+ group.add_argument(
30
+ "diff_file",
31
+ nargs="?",
32
+ help="Path to a file containing the unified diff."
33
+ )
34
+ parser.add_argument(
35
+ "--project-dir",
36
+ type=str,
37
+ default=".",
38
+ help="Project directory where the diff should be applied (default: current directory)."
39
+ )
40
+ parser.add_argument('--nobeep', action='store_false', dest='beep', default=True, help='Disable completion notification beep')
41
+ parser.add_argument(
42
+ "--model",
43
+ type=str,
44
+ default=None,
45
+ help="Model to use for applying the diff"
46
+ )
47
+ parser.add_argument(
48
+ "--max_tokens",
49
+ type=int,
50
+ default=30000,
51
+ help="Maximum tokens to use for LLM responses"
52
+ )
53
+ return parser.parse_args()
54
+
55
+ def main():
56
+ args = parse_arguments()
57
+ if args.diff:
58
+ diff_text = args.diff
59
+ else:
60
+ diff_path = Path(args.diff_file)
61
+ if not diff_path.exists():
62
+ print(f"Error: Diff file '{args.diff_file}' does not exist.")
63
+ sys.exit(1)
64
+ diff_text = diff_path.read_text(encoding="utf8")
65
+
66
+ project_dir = args.project_dir
67
+ success = apply_diff(project_dir, diff_text)
68
+ if success:
69
+ print("✅ Diff applied successfully.")
70
+ else:
71
+ print("❌ Failed to apply diff using git apply. Attempting smart apply.")
72
+ from gptdiff.gptdiff import smart_apply_patch
73
+ smart_apply_patch(project_dir, diff_text, "", args)
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.11
3
+ Version: 0.1.14
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
@@ -0,0 +1,9 @@
1
+ gptdiff/__init__.py,sha256=yGjgwv7tNvH1ZLPsQyoo1CxpTOl1iCAwwDBp-_17ksQ,89
2
+ gptdiff/gptdiff.py,sha256=JWdlnnMZPmDOsg6jD2QQandaKZhpNlk8lQEgs2KmmM0,36872
3
+ gptdiff/gptpatch.py,sha256=Z8CWWIfIL2o7xPLVdhzN5GSyJq0vsK4XQRzu4hMWNQk,2194
4
+ gptdiff-0.1.14.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
5
+ gptdiff-0.1.14.dist-info/METADATA,sha256=imeKq8Q_B_PS4S0lMFvWAkoPxqWCZZW6OciaEKDsA3Q,7799
6
+ gptdiff-0.1.14.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
7
+ gptdiff-0.1.14.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
8
+ gptdiff-0.1.14.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
9
+ gptdiff-0.1.14.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  gptdiff = gptdiff.gptdiff:main
3
+ gptpatch = gptdiff.gptpatch:main
@@ -1,8 +0,0 @@
1
- gptdiff/__init__.py,sha256=yGjgwv7tNvH1ZLPsQyoo1CxpTOl1iCAwwDBp-_17ksQ,89
2
- gptdiff/gptdiff.py,sha256=HbnFkP1o5jQ-WIC99y-et_X6BeBsArtKqiFb2FG6X28,36694
3
- gptdiff-0.1.11.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
4
- gptdiff-0.1.11.dist-info/METADATA,sha256=pCTb2Mu4w0Y6HSAY5A8F8jwrJrLfq_SQw80PyQvRIXA,7799
5
- gptdiff-0.1.11.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
6
- gptdiff-0.1.11.dist-info/entry_points.txt,sha256=0yvXYEVAZFI-p32kQ4-h3qKVWS0a86jsM9FAwF89t9w,49
7
- gptdiff-0.1.11.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
8
- gptdiff-0.1.11.dist-info/RECORD,,