gptdiff 0.1.11__py3-none-any.whl → 0.1.14__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/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,,