From 9c19d5f7f8c2e32e274ff7a6fca865ae0c1d0d09 Mon Sep 17 00:00:00 2001 From: weiconghe <13976098570@163.com> Date: Tue, 26 May 2026 15:30:52 +0800 Subject: [PATCH] fix: allow non-existent paths in ignore check The _is_ignored_relative_path method used to raise FileNotFoundError when checking a path that does not exist on disk. This prevented editing tools (replace_content, insert_after_symbol, etc.) from operating on newly created files. Since a non-existent file cannot be matched by ignore patterns, returning False (not ignored) is semantically correct and safe. Changes: - _is_ignored_relative_path: return False instead of raising for non-existent paths, with a debug log message - validate_relative_path docstring: updated to reflect new behavior - Added 3 test cases covering non-existent path scenarios --- CHANGELOG.md | 3 +++ src/serena/project.py | 14 ++++++----- .../config/test_global_ignored_paths.py | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c99726..4411a500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* General: + - Not existing paths return `False` on is ignored checks (instead of raising an error) + # v1.5.1 (2026-05-18) * General: diff --git a/src/serena/project.py b/src/serena/project.py index f202af96..2c1558a8 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -191,8 +191,8 @@ class Project(ToStringMixin): def _is_ignored_relative_path(self, relative_path: str | Path, ignore_non_source_files: bool = True) -> bool: """ - Determine whether an existing path should be ignored based on file type and ignore patterns. - Raises `FileNotFoundError` if the path does not exist. + Determine whether a path should be ignored based on file type and ignore patterns. + Returns False for non-existent paths since they cannot be matched by ignore patterns. :param relative_path: Relative path to check :param ignore_non_source_files: whether files that are not source files (according to the file masks @@ -208,7 +208,8 @@ class Project(ToStringMixin): abs_path = os.path.join(self.project_root, relative_path) if not os.path.exists(abs_path): - raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed") + log.debug(f"Path {abs_path} does not exist, skipping ignore check") + return False # Check file extension if it's a file is_file = os.path.isfile(abs_path) @@ -284,10 +285,11 @@ class Project(ToStringMixin): def validate_relative_path(self, relative_path: str, require_not_ignored: bool = False) -> None: """ - Validates that the given relative path to an existing file/dir is safe to read or edit, - meaning it's inside the project directory. + Validates that the given relative path is safe to read or edit, + meaning it's inside the project directory and not ignored. - Passing a path to a non-existing file will lead to a `FileNotFoundError`. + Non-existent paths are allowed (not considered ignored) to support + editing tools that create new files. :param relative_path: the path to validate, relative to the project root :param require_not_ignored: if True, the path must not be ignored according to the project's ignore settings diff --git a/test/serena/config/test_global_ignored_paths.py b/test/serena/config/test_global_ignored_paths.py index af33ceea..da9fadb9 100644 --- a/test/serena/config/test_global_ignored_paths.py +++ b/test/serena/config/test_global_ignored_paths.py @@ -102,6 +102,31 @@ class TestGlobalIgnoredPaths: assert project.is_ignored_path(str(self.project_path / "debug.log")) assert not project.is_ignored_path(str(self.project_path / "main.py")) + def test_nonexistent_path_not_ignored(self) -> None: + """Non-existent paths should return False (not ignored), not raise FileNotFoundError.""" + project = _create_test_project(self.project_path) + nonexistent = str(self.project_path / "src" / "new_file.py") + assert not os.path.exists(nonexistent) + assert not project.is_ignored_path(nonexistent) + + def test_nonexistent_path_with_ignore_patterns(self) -> None: + """Non-existent paths matching ignore patterns still return False (cannot check).""" + project = _create_test_project( + self.project_path, + global_ignored_paths=["*.log"], + ) + nonexistent = str(self.project_path / "nonexistent.log") + assert not os.path.exists(nonexistent) + # Should not raise, should not match the *.log pattern + assert not project.is_ignored_path(nonexistent) + + def test_validate_relative_path_allows_nonexistent(self) -> None: + """validate_relative_path with require_not_ignored should not raise for non-existent paths.""" + project = _create_test_project(self.project_path) + nonexistent = "src/new_file.py" + # Should not raise + project.validate_relative_path(nonexistent, require_not_ignored=True) + class TestRegisteredProjectGlobalIgnoredPaths: """RegisteredProject.get_project_instance() correctly passes global patterns to Project."""