From 0acfb3288d086cc0feda2dca728848b9c3f1debe Mon Sep 17 00:00:00 2001 From: "Xingdi (Eric) Yuan" Date: Mon, 4 Nov 2024 13:38:15 -0500 Subject: [PATCH 1/2] substitution patcher with file path --- froggy/envs/env.py | 5 +- froggy/tools/patchers.py | 150 +++++++++++++++++++++++---------------- froggy/tools/pdb.py | 8 ++- 3 files changed, 100 insertions(+), 63 deletions(-) diff --git a/froggy/envs/env.py b/froggy/envs/env.py index c9032f4a6..e4f973cac 100644 --- a/froggy/envs/env.py +++ b/froggy/envs/env.py @@ -221,7 +221,10 @@ def run(self): def load_current_file(self, filepath: str) -> bool: self.current_file = filepath - self.current_file_content = (self.working_dir / self.current_file).read_text() + self.current_file_content = self.load_file(filepath) + + def load_file(self, filepath: str) -> str: + return (self.working_dir / filepath).read_text() def directory_tree(self, root: str = None, editable_only: bool = False): root = Path(root or self.path).absolute() diff --git a/froggy/tools/patchers.py b/froggy/tools/patchers.py index 3b52fc97d..60516b183 100644 --- a/froggy/tools/patchers.py +++ b/froggy/tools/patchers.py @@ -139,82 +139,112 @@ class SubstitutionPatcher(CodePatcher): name = "substitution_patcher" description = "Creates patches of code given start and end lines." instructions = { - "template": "```rewrite head:tail new_code```", - "description": "Rewrite the code between lines [head, tail] with the new code. Line numbers are 1-based. When tail is not provided, it's assumed to be the same as head. When head is not provided, it's assumed to rewrite the whole code. The new code should be valid python code include proper indentation (can be determined from context), the special tokens and are used to wrap the new code. ", + "template": "```rewrite file/path.py head:tail new_code```", + "description": "Rewrite the code in file/path.py between lines [head, tail] with the new code. Line numbers are 1-based. When file path is not provided, it's assumed to rewrite the current file. When head and tail are not provided, it's assumed to rewrite the whole code. When only head is provided, it's assumed to rewrite that single line. The new code should be valid python code include proper indentation (can be determined from context), the special tokens and are used to wrap the new code. ", "examples": [ - "```rewrite print('hola')``` will rewrite the entire code to be print('hola'), because no line number is provided.", - "```rewrite 10 print('bonjour')``` will rewite line number 10 to be print('bonjour'), with the indents ahead (in this case, 4 spaces).", - "```rewrite 10:20 print('hello')\\n print('hi again')``` will replace the chunk of code between line number 10 and 20 by the two lines provided, both with indents ahead (in this case, 4 spaces).", + "```rewrite print('hola')``` will rewrite the current file (the entire code) to be print('hola'), because no line number is provided.", + "```rewrite 10 print('bonjour')``` will rewite line number 10 of the current file to be print('bonjour'), with the indents ahead (in this case, 4 spaces).", + "```rewrite 10:20 print('hello')\\n print('hi again')``` will replace the chunk of code between line number 10 and 20 in the current file by the two lines provided, both with indents ahead (in this case, 4 spaces).", + "```rewrite code/utils.py 4:6 print('buongiorno')``` will replace the chunk of code between line number 4 and 6 in the file code/utils.py by the single line provided, with the indent ahead (in this case, 8 spaces).", ], } def is_triggered(self, action): return action.startswith(self.action) - def use(self, patch): - content = patch.split(self.action)[1].split("```")[0].strip() - self.rewrite_success = False - # rewrite the code given what's been discovered during the debugging - if self.environment.current_file is None: - return "No file is currently open." - if self.environment.current_file not in self.environment.editable_files: - return f"File {self.environment.current_file} is not editable." + def _rewrite_file(self, file_path, head, tail, new_code): + if file_path is None: + # by default, rewrite the current file + file_path = self.environment.current_file + + if file_path is None: + return "No file is currently open.", False, None + if file_path.startswith(str(self.environment.working_dir)): + file_path = file_path[len(str(self.environment.working_dir)) + 1 :] + if file_path not in self.environment.all_files: + return f"File {file_path} does not exist or is not in the current repository.", False, None + if file_path not in self.environment.editable_files: + return f"File {file_path} is not editable.", False, None success = True - if content[0].isnumeric(): - # the line number is provided + new_code = clean_code(new_code) # str + new_code_lines = new_code.split("\n") + new_code_length = len(new_code_lines) # number of lines in the newly generated code + if head is None and tail is None: + # no line number is provided, rewrite the whole code try: - line_numbers = content.split("")[0].strip() - content = ( - content[len(line_numbers) :] - .strip() - .split("")[1] - .split("")[0] - ) # get the new code - line_numbers = line_numbers.split(":") - line_numbers = [item.strip() for item in line_numbers] - if len(line_numbers) == 1: - # only head is provided (rewrite that line) - head = int(line_numbers[0]) - 1 # 1-based to 0-based - tail = head - elif len(line_numbers) == 2: - # both head and tail are provided - head = int(line_numbers[0]) - 1 # 1-based to 0-based - tail = int(line_numbers[1]) - 1 # 1-based to 0-based - else: - success = False + self.environment.overwrite_file(filepath=file_path, content=new_code) + if file_path == self.environment.current_file: + self.environment.load_current_file(file_path) except: success = False - if success is True: - # rewrite the code - content = clean_code(content) # str - content_lines = content.split("\n") - content_length = len( - content_lines - ) # number of lines in the newly generated code - full_code_lines = self.environment.current_file_content.split("\n") - full_code_lines[head : tail + 1] = content_lines # list - self.environment.overwrite_file( - filepath=self.environment.current_file, - content="\n".join(full_code_lines), - ) - self.environment.load_current_file(self.environment.current_file) else: - # no line number is provided, rewrite the whole code + # rewrite the code given the provided line numbers + if tail is None: + # only head is provided (rewrite that line) + tail = head try: - content = content.split("")[1].split("")[0] # get the new code - content = clean_code(content) # str - content_length = len( - content.split("\n") - ) # number of lines in the newly generated code + # rewrite the code + full_code_lines = self.environment.load_file(file_path).split("\n") + full_code_lines[head : tail + 1] = new_code_lines # list self.environment.overwrite_file( - filepath=self.environment.current_file, content=content + filepath=file_path, + content="\n".join(full_code_lines) ) - self.environment.load_current_file(self.environment.current_file) - head, tail = None, None + if file_path == self.environment.current_file: + self.environment.load_current_file(file_path) except: success = False + return "", success, new_code_length + def parse_line_numbers(self, line_number_string): + + # only line number is provided + line_numbers = line_number_string.split(":") + line_numbers = [item.strip() for item in line_numbers] + assert len(line_numbers) in [1, 2], "Invalid line number format." + if len(line_numbers) == 1: + # only head is provided (rewrite that line) + head = int(line_numbers[0]) - 1 # 1-based to 0-based + tail = head + else: + # len(line_numbers) == 2: + # both head and tail are provided + head = int(line_numbers[0]) - 1 # 1-based to 0-based + tail = int(line_numbers[1]) - 1 # 1-based to 0-based + return head, tail + + def use(self, patch): + content = patch.split(self.action)[1].split("```")[0].strip() + # parse content to get file_path, head, tail, and new_code + # code/utils.py 4:6 print('buongiorno') + file_path, head, tail = None, None, None + try: + new_code = content.split("", 1)[1].split("", 1)[0] + content = content.split("", 1)[0].strip() + # code/utils.py 4:6 + content_list = content.split() + if len(content_list) == 0: + # no file path and line number is provided + pass + elif len(content_list) == 1: + # either file path or line number is provided + if content_list[0][0].isnumeric(): + # only line number is provided + head, tail = self.parse_line_numbers(content_list[0]) + else: + # only file path is provided + file_path = content_list[0] + elif len(content_list) == 2: + # both file path and line number are provided + file_path = content_list[0] + head, tail = self.parse_line_numbers(content_list[1]) + else: + raise ValueError("Invalid content format.") + except: + return "Rewrite failed." + + message, success, new_code_length = self._rewrite_file(file_path, head, tail, new_code) if success is True: if ( hasattr(self.environment, "tools") @@ -222,11 +252,11 @@ def use(self, patch): and "pdb" in self.environment.tools ): self.environment.tools["pdb"].breakpoint_modify( + file_path, head + 1 if isinstance(head, int) else None, tail + 1 if isinstance(tail, int) else None, - content_length, + new_code_length, ) # converting head/tail back to 1-based index for breakpoint management self.rewrite_success = True return "Rewriting done." - - return "Rewrite failed." + return "\n".join([message, "Rewrite failed."]) diff --git a/froggy/tools/pdb.py b/froggy/tools/pdb.py index 86b8f316f..df425932b 100644 --- a/froggy/tools/pdb.py +++ b/froggy/tools/pdb.py @@ -240,7 +240,7 @@ def breakpoint_add_clear(self, action: str, which_file=None): return success, output - def breakpoint_modify(self, rewrite_head, rewrite_tail, new_code_length): + def breakpoint_modify(self, rewrite_file, rewrite_head, rewrite_tail, new_code_length): # handle breakpoints line number changes caused by rewriting # this is a wrapper that manages the self.breakpoints_state, which does not reset at each pseudo terminal start # self.breakpoints_state is a dict, the keys are "|||".join([file_path, str(line_number)]) and values are breakpoint_command @@ -249,9 +249,13 @@ def breakpoint_modify(self, rewrite_head, rewrite_tail, new_code_length): current_breakpoints_state_copy = copy.copy( self.environment.current_breakpoints_state ) + if rewrite_file is None: + rewrite_file = self.environment.current_file + if rewrite_file.startswith(str(self.environment.working_dir)): + rewrite_file = rewrite_file[len(str(self.environment.working_dir)) + 1 :] for _key in self.environment.current_breakpoints_state.keys(): _file_path, _line_number = _key.split("|||") - if _file_path != self.environment.current_file: + if _file_path != rewrite_file: # the breakpoints are not in the current file, no need to modify continue _line_number = int(_line_number) From d239a17a377dd818125f53dac1e555ff9e0f0bfa Mon Sep 17 00:00:00 2001 From: "Xingdi (Eric) Yuan" Date: Mon, 4 Nov 2024 13:58:08 -0500 Subject: [PATCH 2/2] rewrite success --- froggy/tools/patchers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/froggy/tools/patchers.py b/froggy/tools/patchers.py index 60516b183..8530a49d1 100644 --- a/froggy/tools/patchers.py +++ b/froggy/tools/patchers.py @@ -242,6 +242,7 @@ def use(self, patch): else: raise ValueError("Invalid content format.") except: + self.rewrite_success = False return "Rewrite failed." message, success, new_code_length = self._rewrite_file(file_path, head, tail, new_code) @@ -259,4 +260,6 @@ def use(self, patch): ) # converting head/tail back to 1-based index for breakpoint management self.rewrite_success = True return "Rewriting done." + + self.rewrite_success = False return "\n".join([message, "Rewrite failed."])