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
This commit is contained in:
weiconghe authored and Michael Panchenko committed 2026-05-26 11:44:06 +02:00
1 parent 981f560fa3
commit 9c19d5f7f8
3 files changed
+36 -6

No files matched your search

+3
View File
@@ -2,6 +2,9 @@
Status of the `main` branch. Changes prior to the next official version change will appear here. 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) # v1.5.1 (2026-05-18)
* General: * General:
+8 -6
View File
@@ -191,8 +191,8 @@ class Project(ToStringMixin):
def _is_ignored_relative_path(self, relative_path: str | Path, ignore_non_source_files: bool = True) -> bool: 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. Determine whether a path should be ignored based on file type and ignore patterns.
Raises `FileNotFoundError` if the path does not exist. Returns False for non-existent paths since they cannot be matched by ignore patterns.
:param relative_path: Relative path to check :param relative_path: Relative path to check
:param ignore_non_source_files: whether files that are not source files (according to the file masks :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) abs_path = os.path.join(self.project_root, relative_path)
if not os.path.exists(abs_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 # Check file extension if it's a file
is_file = os.path.isfile(abs_path) 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: 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, Validates that the given relative path is safe to read or edit,
meaning it's inside the project directory. 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 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 :param require_not_ignored: if True, the path must not be ignored according to the project's ignore settings
@@ -102,6 +102,31 @@ class TestGlobalIgnoredPaths:
assert project.is_ignored_path(str(self.project_path / "debug.log")) assert project.is_ignored_path(str(self.project_path / "debug.log"))
assert not project.is_ignored_path(str(self.project_path / "main.py")) 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: class TestRegisteredProjectGlobalIgnoredPaths:
"""RegisteredProject.get_project_instance() correctly passes global patterns to Project.""" """RegisteredProject.get_project_instance() correctly passes global patterns to Project."""