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 +81 -68
- gptdiff/gptpatch.py +76 -0
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/METADATA +1 -1
- gptdiff-0.1.14.dist-info/RECORD +9 -0
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/entry_points.txt +1 -0
- gptdiff-0.1.11.dist-info/RECORD +0 -8
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/LICENSE.txt +0 -0
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/WHEEL +0 -0
- {gptdiff-0.1.11.dist-info → gptdiff-0.1.14.dist-info}/top_level.txt +0 -0
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 =
|
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")
|
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
|
-
|
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()
|
@@ -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,,
|
gptdiff-0.1.11.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|