From 8464f257aafaa5b66b63aae6921bdcc800c90b4e Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Mon, 11 Aug 2025 16:31:40 +0200 Subject: [PATCH] Hotfix: fix create_text_file (had raised FileNotFound on new files), fix return type of the tool --- src/serena/mcp.py | 2 +- src/serena/project.py | 17 +++++++++++++++-- src/serena/tools/file_tools.py | 14 ++++++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 5de1e11a..fa86a334 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -71,7 +71,7 @@ class SerenaMCPFactory: """ s = deepcopy(schema) - def walk(node): + def walk(node): # type: ignore if not isinstance(node, dict): # lists get handled by parent calls return node diff --git a/src/serena/project.py b/src/serena/project.py index 117e3535..cc7b12f6 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -89,7 +89,8 @@ class Project: def _is_ignored_relative_path(self, relative_path: str | Path, ignore_non_source_files: bool = True) -> bool: """ - Determine whether a path should be ignored based on file type and ignore patterns. + Determine whether an existing path should be ignored based on file type and ignore patterns. + Raises `FileNotFoundError` if the path does not exist. :param relative_path: Relative path to check :param ignore_non_source_files: whether files that are not source files (according to the file masks @@ -156,10 +157,22 @@ class Project: path = path.resolve() return path.is_relative_to(_proj_root) + def relative_path_exists(self, relative_path: str) -> bool: + """ + Checks if the given relative path exists in the project directory. + + :param relative_path: the path to check, relative to the project root + :return: True if the path exists, False otherwise + """ + abs_path = Path(self.project_root) / relative_path + return abs_path.exists() + def validate_relative_path(self, relative_path: str) -> None: """ - Validates that the given relative path is safe to read or edit, + Validates that the given relative path to an existing file/dir is safe to read or edit, meaning it's inside the project directory and is not ignored by git. + + Passing a path to a non-existing file will lead to a `FileNotFoundError`. """ if not self.is_path_in_project(relative_path): raise ValueError(f"{relative_path=} points to path outside of the repository root; cannot access for safety reasons") diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index 6e3944ba..9b5f5bbe 100644 --- a/src/serena/tools/file_tools.py +++ b/src/serena/tools/file_tools.py @@ -65,17 +65,23 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit): :param content: the (utf-8-encoded) content to write to the file :return: a message indicating success or failure """ - self.project.validate_relative_path(relative_path) - - abs_path = (Path(self.get_project_root()) / relative_path).resolve() + project_root = self.get_project_root() + abs_path = (Path(project_root) / relative_path).resolve() will_overwrite_existing = abs_path.exists() + if will_overwrite_existing: + self.project.validate_relative_path(relative_path) + else: + assert abs_path.is_relative_to( + self.get_project_root() + ), f"Cannot create file outside of the project directory, got {relative_path=}" + abs_path.parent.mkdir(parents=True, exist_ok=True) abs_path.write_text(content, encoding="utf-8") answer = f"File created: {relative_path}." if will_overwrite_existing: answer += " Overwrote existing file." - return answer + return json.dumps(answer) class ListDirTool(Tool):