style: Run linter on patch_flex_coder.py

This commit is contained in:
Paul Gauthier (aider) 2025-04-14 15:29:14 -07:00
parent 5573cdfba1
commit 4e1e77890b

View file

@ -10,7 +10,6 @@ from ..dump import dump # noqa: F401
from .base_coder import Coder from .base_coder import Coder
from .patch_prompts import PatchPrompts from .patch_prompts import PatchPrompts
# Import search_replace utilities # Import search_replace utilities
from .search_replace import editblock_strategies, flexible_search_and_replace from .search_replace import editblock_strategies, flexible_search_and_replace
@ -167,7 +166,7 @@ def _peek_change_hunk(
elif line_type == "delete": elif line_type == "delete":
# This implies interleaved +/- lines which this simplified parser doesn't handle well. # This implies interleaved +/- lines which this simplified parser doesn't handle well.
# Treat as end of hunk? Or raise error? Let's treat as end for now. # Treat as end of hunk? Or raise error? Let's treat as end for now.
index = current_line_index # Put the delete line back for the next hunk index = current_line_index # Put the delete line back for the next hunk
break break
else: else:
raise DiffError(f"Unexpected line type '{line_type}' in mode '{mode}': {line}") raise DiffError(f"Unexpected line type '{line_type}' in mode '{mode}': {line}")
@ -200,17 +199,17 @@ def _peek_change_hunk(
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# PatchFlexCoder Class Implementation # PatchFlexCoder Class Implementation
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
class PatchFlexCoder(Coder): # Rename class class PatchFlexCoder(Coder): # Rename class
""" """
A coder that uses the patch format for LLM output, but applies changes A coder that uses the patch format for LLM output, but applies changes
using flexible search-and-replace logic for UPDATE actions, ignoring @@ hints using flexible search-and-replace logic for UPDATE actions, ignoring @@ hints
and precise line numbers during application. and precise line numbers during application.
""" """
edit_format = "patch-flex" # Give it a distinct name edit_format = "patch-flex" # Give it a distinct name
gpt_prompts = PatchPrompts() # Use the same prompts as PatchCoder gpt_prompts = PatchPrompts() # Use the same prompts as PatchCoder
def get_edits(self) -> List[ParsedEdit]: # Return type changed def get_edits(self) -> List[ParsedEdit]: # Return type changed
""" """
Parses the LLM response content (containing the patch) into a list of Parses the LLM response content (containing the patch) into a list of
ParsedEdit objects, extracting search/replace blocks for UPDATEs. ParsedEdit objects, extracting search/replace blocks for UPDATEs.
@ -221,13 +220,10 @@ class PatchFlexCoder(Coder): # Rename class
lines = content.splitlines() lines = content.splitlines()
start_index = 0 start_index = 0
if ( if len(lines) >= 2 and _norm(lines[0]).startswith("*** Begin Patch"):
len(lines) >= 2
and _norm(lines[0]).startswith("*** Begin Patch")
):
start_index = 1 start_index = 1
else: else:
# Tolerate missing sentinels if content looks like a patch action # Tolerate missing sentinels if content looks like a patch action
is_patch_like = any( is_patch_like = any(
_norm(line).startswith( _norm(line).startswith(
("@@", "*** Update File:", "*** Add File:", "*** Delete File:") ("@@", "*** Update File:", "*** Add File:", "*** Delete File:")
@ -237,9 +233,7 @@ class PatchFlexCoder(Coder): # Rename class
if not is_patch_like: if not is_patch_like:
self.io.tool_warning("Response does not appear to be in patch format.") self.io.tool_warning("Response does not appear to be in patch format.")
return [] return []
self.io.tool_warning( self.io.tool_warning("Patch format warning: Missing '*** Begin Patch' sentinel.")
"Patch format warning: Missing '*** Begin Patch' sentinel."
)
# Identify files needed for context lookups (only for DELETE check) # Identify files needed for context lookups (only for DELETE check)
needed_paths = identify_files_needed(content) needed_paths = identify_files_needed(content)
@ -248,7 +242,6 @@ class PatchFlexCoder(Coder): # Rename class
# We read content dynamically in apply_edits. # We read content dynamically in apply_edits.
known_files = set(self.get_inchat_relative_files()) | set(needed_paths) known_files = set(self.get_inchat_relative_files()) | set(needed_paths)
try: try:
# Parse the patch text into ParsedEdit objects # Parse the patch text into ParsedEdit objects
parsed_edits = self._parse_patch_text(lines, start_index, known_files) parsed_edits = self._parse_patch_text(lines, start_index, known_files)
@ -272,7 +265,7 @@ class PatchFlexCoder(Coder): # Rename class
while index < len(lines): while index < len(lines):
line = lines[index] line = lines[index]
norm_line = _norm(line) norm_line = _norm(line)
line_num = index + 1 # 1-based for reporting line_num = index + 1 # 1-based for reporting
if norm_line == "*** End Patch": if norm_line == "*** End Patch":
index += 1 index += 1
@ -289,7 +282,7 @@ class PatchFlexCoder(Coder): # Rename class
# self.io.tool_warning(f"Update File target '{path}' not found in chat context.") # self.io.tool_warning(f"Update File target '{path}' not found in chat context.")
current_file_path = path current_file_path = path
current_move_path = None # Reset move path for new file current_move_path = None # Reset move path for new file
# Check for optional Move to immediately after # Check for optional Move to immediately after
if index < len(lines) and _norm(lines[index]).startswith("*** Move to: "): if index < len(lines) and _norm(lines[index]).startswith("*** Move to: "):
@ -298,7 +291,7 @@ class PatchFlexCoder(Coder): # Rename class
if not move_to: if not move_to:
raise DiffError(f"Move to action missing path (line {index}).") raise DiffError(f"Move to action missing path (line {index}).")
current_move_path = move_to current_move_path = move_to
continue # Continue to parse hunks for this file continue # Continue to parse hunks for this file
# ---------- DELETE ---------- # # ---------- DELETE ---------- #
elif norm_line.startswith("*** Delete File: "): elif norm_line.startswith("*** Delete File: "):
@ -307,11 +300,13 @@ class PatchFlexCoder(Coder): # Rename class
if not path: if not path:
raise DiffError(f"Delete File action missing path (line {line_num}).") raise DiffError(f"Delete File action missing path (line {line_num}).")
if path not in known_files: if path not in known_files:
# Check against known files before adding delete action # Check against known files before adding delete action
self.io.tool_warning(f"Delete File target '{path}' not found in chat context.") self.io.tool_warning(f"Delete File target '{path}' not found in chat context.")
parsed_edits.append(ParsedEdit(path=path, type=ActionType.DELETE, patch_line_num=line_num)) parsed_edits.append(
current_file_path = None # Reset current file context ParsedEdit(path=path, type=ActionType.DELETE, patch_line_num=line_num)
)
current_file_path = None # Reset current file context
current_move_path = None current_move_path = None
continue continue
@ -328,13 +323,13 @@ class PatchFlexCoder(Coder): # Rename class
action.path = path action.path = path
action.patch_line_num = line_num action.patch_line_num = line_num
parsed_edits.append(action) parsed_edits.append(action)
current_file_path = None # Reset current file context current_file_path = None # Reset current file context
current_move_path = None current_move_path = None
continue continue
# ---------- Hunks within UPDATE ---------- # # ---------- Hunks within UPDATE ---------- #
elif current_file_path: elif current_file_path:
# Skip @@ lines, they are ignored by this coder's application logic # Skip @@ lines, they are ignored by this coder's application logic
if norm_line.startswith("@@"): if norm_line.startswith("@@"):
index += 1 index += 1
continue continue
@ -348,12 +343,11 @@ class PatchFlexCoder(Coder): # Rename class
ins_lines, ins_lines,
context_after, context_after,
next_index, next_index,
_is_eof, # EOF marker not strictly needed for search/replace logic _is_eof, # EOF marker not strictly needed for search/replace logic
) = _peek_change_hunk(lines, index) ) = _peek_change_hunk(lines, index)
except DiffError as e: except DiffError as e:
raise DiffError(f"{e} (near line {line_num} in patch)") raise DiffError(f"{e} (near line {line_num} in patch)")
if not del_lines and not ins_lines: if not del_lines and not ins_lines:
# Skip hunks that contain only context - they don't represent a change # Skip hunks that contain only context - they don't represent a change
index = next_index index = next_index
@ -371,15 +365,14 @@ class PatchFlexCoder(Coder): # Rename class
# the original block likely ended with the last deleted line. # the original block likely ended with the last deleted line.
# Or if context_before/del/ins are all empty, it's just context. # Or if context_before/del/ins are all empty, it's just context.
if not context_after and (del_lines or ins_lines): if not context_after and (del_lines or ins_lines):
search_text += "\n" search_text += "\n"
# Replace text already includes context_after, so only add if that was empty too # Replace text already includes context_after, so only add if that was empty too
if not ins_lines: if not ins_lines:
replace_text += "\n" replace_text += "\n"
elif context_after or context_before or del_lines or ins_lines: elif context_after or context_before or del_lines or ins_lines:
# If there's any content, ensure trailing newline for consistency # If there's any content, ensure trailing newline for consistency
search_text += "\n" search_text += "\n"
replace_text += "\n" replace_text += "\n"
parsed_edits.append( parsed_edits.append(
ParsedEdit( ParsedEdit(
@ -387,7 +380,7 @@ class PatchFlexCoder(Coder): # Rename class
type=ActionType.UPDATE, type=ActionType.UPDATE,
search_text=search_text, search_text=search_text,
replace_text=replace_text, replace_text=replace_text,
move_path=current_move_path, # Carry over move path for this hunk move_path=current_move_path, # Carry over move path for this hunk
patch_line_num=hunk_start_index + 1, patch_line_num=hunk_start_index + 1,
) )
) )
@ -395,11 +388,13 @@ class PatchFlexCoder(Coder): # Rename class
continue continue
# If we are here, the line is unexpected or misplaced # If we are here, the line is unexpected or misplaced
if not norm_line.strip(): # Allow blank lines between actions/files if not norm_line.strip(): # Allow blank lines between actions/files
index += 1 index += 1
continue continue
raise DiffError(f"Unknown or misplaced line while parsing patch (line {line_num}): {line}") raise DiffError(
f"Unknown or misplaced line while parsing patch (line {line_num}): {line}"
)
return parsed_edits return parsed_edits
@ -423,7 +418,7 @@ class PatchFlexCoder(Coder): # Rename class
if not line.startswith("+"): if not line.startswith("+"):
if norm_line.strip() == "": if norm_line.strip() == "":
added_lines.append("") # Treat blank line as adding a blank line added_lines.append("") # Treat blank line as adding a blank line
else: else:
raise DiffError(f"Invalid Add File line (missing '+') (line {index+1}): {line}") raise DiffError(f"Invalid Add File line (missing '+') (line {index+1}): {line}")
else: else:
@ -432,14 +427,14 @@ class PatchFlexCoder(Coder): # Rename class
index += 1 index += 1
action = ParsedEdit( action = ParsedEdit(
path="", # Path set by caller path="", # Path set by caller
type=ActionType.ADD, type=ActionType.ADD,
new_content="\n".join(added_lines), new_content="\n".join(added_lines),
patch_line_num=start_line_num patch_line_num=start_line_num,
) )
return action, index return action, index
def apply_edits(self, edits: List[ParsedEdit]): # Argument type changed def apply_edits(self, edits: List[ParsedEdit]): # Argument type changed
""" """
Applies the parsed edits. Uses flexible search-and-replace for UPDATEs. Applies the parsed edits. Uses flexible search-and-replace for UPDATEs.
""" """
@ -456,7 +451,7 @@ class PatchFlexCoder(Coder): # Rename class
path_obj = pathlib.Path(full_path) path_obj = pathlib.Path(full_path)
current_content = None current_content = None
edit_failed = False edit_failed = False
final_move_path = None # Track the last move destination for this file final_move_path = None # Track the last move destination for this file
# Check for simple ADD/DELETE first (should ideally be only one per file) # Check for simple ADD/DELETE first (should ideally be only one per file)
if len(path_edits) == 1 and path_edits[0].type in [ActionType.ADD, ActionType.DELETE]: if len(path_edits) == 1 and path_edits[0].type in [ActionType.ADD, ActionType.DELETE]:
@ -464,8 +459,10 @@ class PatchFlexCoder(Coder): # Rename class
try: try:
if edit.type == ActionType.ADD: if edit.type == ActionType.ADD:
if path_obj.exists(): if path_obj.exists():
# Allow overwrite on ADD? Or error? Let's warn and overwrite. # Allow overwrite on ADD? Or error? Let's warn and overwrite.
self.io.tool_warning(f"ADD Warning: File '{path}' already exists, overwriting.") self.io.tool_warning(
f"ADD Warning: File '{path}' already exists, overwriting."
)
# raise DiffError(f"ADD Error: File already exists: {path}") # raise DiffError(f"ADD Error: File already exists: {path}")
if edit.new_content is None: if edit.new_content is None:
raise DiffError(f"ADD change for {path} has no content") raise DiffError(f"ADD change for {path} has no content")
@ -480,14 +477,18 @@ class PatchFlexCoder(Coder): # Rename class
elif edit.type == ActionType.DELETE: elif edit.type == ActionType.DELETE:
self.io.tool_output(f"Deleting {path}") self.io.tool_output(f"Deleting {path}")
if not path_obj.exists(): if not path_obj.exists():
self.io.tool_warning(f"DELETE Warning: File not found, skipping: {path}") self.io.tool_warning(
f"DELETE Warning: File not found, skipping: {path}"
)
else: else:
path_obj.unlink() path_obj.unlink()
except (DiffError, FileNotFoundError, IOError, OSError) as e: except (DiffError, FileNotFoundError, IOError, OSError) as e:
raise ValueError(f"Error applying action '{edit.type}' to {path}: {e}") raise ValueError(f"Error applying action '{edit.type}' to {path}: {e}")
except Exception as e: except Exception as e:
raise ValueError(f"Unexpected error applying action '{edit.type}' to {path}: {e}") raise ValueError(
continue # Move to the next file path f"Unexpected error applying action '{edit.type}' to {path}: {e}"
)
continue # Move to the next file path
# --- Handle UPDATE actions sequentially --- # --- Handle UPDATE actions sequentially ---
self.io.tool_output(f"Updating {path}...") self.io.tool_output(f"Updating {path}...")
@ -500,13 +501,17 @@ class PatchFlexCoder(Coder): # Rename class
for i, edit in enumerate(path_edits): for i, edit in enumerate(path_edits):
if edit.type != ActionType.UPDATE: if edit.type != ActionType.UPDATE:
raise DiffError(f"Unexpected action type '{edit.type}' mixed with UPDATE for {path}") raise DiffError(
f"Unexpected action type '{edit.type}' mixed with UPDATE for {path}"
)
if edit.search_text is None or edit.replace_text is None: if edit.search_text is None or edit.replace_text is None:
raise DiffError(f"UPDATE action for {path} is missing search/replace text") raise DiffError(f"UPDATE action for {path} is missing search/replace text")
final_move_path = edit.move_path # Last move path specified wins final_move_path = edit.move_path # Last move path specified wins
self.io.tool_output(f" Applying hunk {i+1} (from patch line {edit.patch_line_num})...") self.io.tool_output(
f" Applying hunk {i+1} (from patch line {edit.patch_line_num})..."
)
texts = (edit.search_text, edit.replace_text, current_content) texts = (edit.search_text, edit.replace_text, current_content)
new_content = flexible_search_and_replace(texts, editblock_strategies) new_content = flexible_search_and_replace(texts, editblock_strategies)
@ -515,11 +520,11 @@ class PatchFlexCoder(Coder): # Rename class
edit_failed = True edit_failed = True
# Provide more context on failure # Provide more context on failure
err_msg = ( err_msg = (
f"Failed to apply update hunk {i+1} (from patch line {edit.patch_line_num})" f"Failed to apply update hunk {i+1} (from patch line"
f" for file {path}. The search block may not have been found" f" {edit.patch_line_num}) for file {path}. The search block may not"
" or the change conflicted.\n" " have been found or the change conflicted.\nSearch"
f"Search block:\n```\n{edit.search_text}```\n" f" block:\n```\n{edit.search_text}```\nReplace"
f"Replace block:\n```\n{edit.replace_text}```" f" block:\n```\n{edit.replace_text}```"
) )
# Raise immediately to stop processing this file # Raise immediately to stop processing this file
raise ValueError(err_msg) raise ValueError(err_msg)
@ -546,7 +551,7 @@ class PatchFlexCoder(Coder): # Rename class
target_path_obj.parent.mkdir(parents=True, exist_ok=True) target_path_obj.parent.mkdir(parents=True, exist_ok=True)
# Ensure trailing newline # Ensure trailing newline
if not current_content.endswith("\n") and current_content != "": if not current_content.endswith("\n") and current_content != "":
current_content += "\n" current_content += "\n"
self.io.write_text(target_full_path, current_content) self.io.write_text(target_full_path, current_content)
# Remove original file *after* successful write if moved # Remove original file *after* successful write if moved
@ -554,11 +559,11 @@ class PatchFlexCoder(Coder): # Rename class
path_obj.unlink() path_obj.unlink()
except (DiffError, FileNotFoundError, IOError, OSError) as e: except (DiffError, FileNotFoundError, IOError, OSError) as e:
# Raise a ValueError to signal failure # Raise a ValueError to signal failure
raise ValueError(f"Error applying UPDATE to {path}: {e}") raise ValueError(f"Error applying UPDATE to {path}: {e}")
except Exception as e: except Exception as e:
# Catch unexpected errors during application # Catch unexpected errors during application
raise ValueError(f"Unexpected error applying UPDATE to {path}: {e}") raise ValueError(f"Unexpected error applying UPDATE to {path}: {e}")
# Remove the _apply_update method as it's replaced by flexible_search_and_replace logic # Remove the _apply_update method as it's replaced by flexible_search_and_replace logic
# def _apply_update(self, text: str, action: PatchAction, path: str) -> str: # def _apply_update(self, text: str, action: PatchAction, path: str) -> str: