diff --git a/cognite_toolkit/_cdf_tk/constants.py b/cognite_toolkit/_cdf_tk/constants.py index 6e1345768a..9a9fa52b93 100644 --- a/cognite_toolkit/_cdf_tk/constants.py +++ b/cognite_toolkit/_cdf_tk/constants.py @@ -47,7 +47,7 @@ DEFAULT_ENV = "dev" # Add any other files below that should be included in a build -EXCL_FILES = ["README.md", DEFAULT_CONFIG_FILE] +EXCL_FILES = ["README.md", DEFAULT_CONFIG_FILE, ".toolkitignore"] # Files to search for variables. SEARCH_VARIABLES_SUFFIX = frozenset([".yaml", "yml", ".sql", ".csv"]) YAML_SUFFIX = frozenset([".yaml", ".yml"]) diff --git a/cognite_toolkit/_cdf_tk/data_classes/_module_directories.py b/cognite_toolkit/_cdf_tk/data_classes/_module_directories.py index 30d742710a..a70cf8f4e8 100644 --- a/cognite_toolkit/_cdf_tk/data_classes/_module_directories.py +++ b/cognite_toolkit/_cdf_tk/data_classes/_module_directories.py @@ -9,7 +9,9 @@ from typing import Any, SupportsIndex, overload from cognite_toolkit._cdf_tk.constants import INDEX_PATTERN +from cognite_toolkit._cdf_tk.feature_flags import Flags from cognite_toolkit._cdf_tk.utils import calculate_directory_hash, iterate_modules, resource_folder_from_path +from cognite_toolkit._cdf_tk.utils.ignore_patterns import create_ignore_parser_for_module from ._module_toml import ModuleToml @@ -85,6 +87,12 @@ def _source_paths_by_resource_folder(self) -> tuple[dict[str, list[Path]], set[s source_paths_by_resource_folder = defaultdict(list) # The directories in the module that are not resource directories. invalid_resource_directory: set[str] = set() + + # Create ignore parser for this module only if feature flag is enabled + ignore_parser = None + if Flags.TOOLKIT_IGNORE.is_enabled(): + ignore_parser = create_ignore_parser_for_module(self.dir) + for filepath in self.source_paths: try: resource_folder = resource_folder_from_path(filepath) @@ -92,7 +100,16 @@ def _source_paths_by_resource_folder(self) -> tuple[dict[str, list[Path]], set[s relative_to_module = filepath.relative_to(self.dir) is_file_in_resource_folder = relative_to_module.parts[0] == filepath.name if not is_file_in_resource_folder: - invalid_resource_directory.add(relative_to_module.parts[0]) + directory_name = relative_to_module.parts[0] + + # Check if directory should be ignored based on .toolkitignore patterns + # Only apply ignore patterns if feature flag is enabled + should_ignore = False + if ignore_parser is not None: + should_ignore = ignore_parser.is_ignored(relative_to_module, is_directory=True) + + if not should_ignore: + invalid_resource_directory.add(directory_name) continue if filepath.is_file(): source_paths_by_resource_folder[resource_folder].append(filepath) diff --git a/cognite_toolkit/_cdf_tk/feature_flags.py b/cognite_toolkit/_cdf_tk/feature_flags.py index 404b71b0f8..9599340ed6 100644 --- a/cognite_toolkit/_cdf_tk/feature_flags.py +++ b/cognite_toolkit/_cdf_tk/feature_flags.py @@ -56,6 +56,10 @@ class Flags(Enum): "visible": True, "description": "Enables the support for external libraries in the config file", } + TOOLKIT_IGNORE: ClassVar[dict[str, Any]] = { # type: ignore[misc] + "visible": True, + "description": "Enables support for .toolkitignore files to suppress warnings for non-resource directories", + } def is_enabled(self) -> bool: return FeatureFlag.is_enabled(self) diff --git a/cognite_toolkit/_cdf_tk/utils/__init__.py b/cognite_toolkit/_cdf_tk/utils/__init__.py index 9d77d0f94d..e6cf71abd3 100644 --- a/cognite_toolkit/_cdf_tk/utils/__init__.py +++ b/cognite_toolkit/_cdf_tk/utils/__init__.py @@ -19,17 +19,27 @@ calculate_hash, calculate_secure_hash, ) +from .ignore_patterns import ( + ToolkitIgnoreParser, + ToolkitIgnorePattern, + create_ignore_parser_for_module, + find_toolkitignore_files, +) from .modules import find_directory_with_subdirectories, iterate_modules, module_from_path, resource_folder_from_path from .sentry_utils import sentry_exception_filter __all__ = [ "GraphQLParser", + "ToolkitIgnoreParser", + "ToolkitIgnorePattern", "YAMLComment", "YAMLWithComments", "calculate_directory_hash", "calculate_hash", "calculate_secure_hash", + "create_ignore_parser_for_module", "find_directory_with_subdirectories", + "find_toolkitignore_files", "flatten_dict", "get_cicd_environment", "humanize_collection", diff --git a/cognite_toolkit/_cdf_tk/utils/ignore_patterns.py b/cognite_toolkit/_cdf_tk/utils/ignore_patterns.py new file mode 100644 index 0000000000..6619bc94e6 --- /dev/null +++ b/cognite_toolkit/_cdf_tk/utils/ignore_patterns.py @@ -0,0 +1,154 @@ +"""Utility for parsing and matching .toolkitignore patterns. + +This module provides functionality to parse .toolkitignore files and match +file/directory paths against gitignore-style patterns. +""" + +import fnmatch +from pathlib import Path +from typing import Iterable + + +class ToolkitIgnorePattern: + """Represents a single ignore pattern from a .toolkitignore file.""" + + def __init__(self, pattern: str, is_negation: bool = False, is_directory_only: bool = False): + self.original_pattern = pattern + self.is_negation = is_negation + self.is_directory_only = is_directory_only + self.pattern = self._normalize_pattern(pattern) + + def _normalize_pattern(self, pattern: str) -> str: + """Normalize a gitignore-style pattern for matching.""" + # Remove leading/trailing whitespace + pattern = pattern.strip() + + # Handle negation + if pattern.startswith("!"): + self.is_negation = True + pattern = pattern[1:] + + # Handle directory-only patterns + if pattern.endswith("/"): + self.is_directory_only = True + pattern = pattern[:-1] + + # Handle leading slash (absolute path from root) + if pattern.startswith("/"): + pattern = pattern[1:] + + return pattern + + def matches(self, path: Path, is_directory: bool = False) -> bool: + """Check if this pattern matches the given path.""" + if self.is_directory_only and not is_directory: + return False + + # Convert path to string for matching + path_str = path.as_posix() + + # Try exact match first + if fnmatch.fnmatch(path_str, self.pattern): + return True + + # Try matching against any parent directory + if "/" not in self.pattern: + # Simple filename pattern - check if it matches any part of the path + return any(fnmatch.fnmatch(part, self.pattern) for part in path.parts) + + # Pattern contains directories - match against full path + return fnmatch.fnmatch(path_str, self.pattern) + + +class ToolkitIgnoreParser: + """Parser for .toolkitignore files.""" + + def __init__(self, patterns: list[ToolkitIgnorePattern] | None = None): + self.patterns = patterns or [] + + @classmethod + def from_file(cls, ignore_file: Path) -> "ToolkitIgnoreParser": + """Create a parser from a .toolkitignore file.""" + patterns = [] + + if not ignore_file.exists(): + return cls(patterns) + + try: + content = ignore_file.read_text(encoding="utf-8") + for line in content.splitlines(): + line = line.strip() + + # Skip empty lines and comments + if not line or line.startswith("#"): + continue + + pattern = ToolkitIgnorePattern(line) + patterns.append(pattern) + + except (OSError, UnicodeDecodeError): + # If we can't read the file, just return empty parser + pass + + return cls(patterns) + + @classmethod + def from_directory(cls, directory: Path, filename: str = ".toolkitignore") -> "ToolkitIgnoreParser": + """Create a parser by looking for ignore files in directory and parent directories.""" + patterns = [] + + # Start from the given directory and walk up to find ignore files + current_dir = directory + while current_dir != current_dir.parent: + ignore_file = current_dir / filename + if ignore_file.exists(): + parser = cls.from_file(ignore_file) + patterns.extend(parser.patterns) + current_dir = current_dir.parent + + return cls(patterns) + + def is_ignored(self, path: Path, is_directory: bool = False) -> bool: + """Check if a path should be ignored based on the loaded patterns.""" + # Start with not ignored + ignored = False + + # Apply patterns in order + for pattern in self.patterns: + if pattern.matches(path, is_directory): + if pattern.is_negation: + ignored = False + else: + ignored = True + + return ignored + + def filter_paths(self, paths: Iterable[Path], check_directory: bool = True) -> list[Path]: + """Filter a list of paths, removing ignored ones.""" + filtered = [] + + for path in paths: + is_dir = path.is_dir() if check_directory else False + if not self.is_ignored(path, is_dir): + filtered.append(path) + + return filtered + + +def find_toolkitignore_files(directory: Path, filename: str = ".toolkitignore") -> list[Path]: + """Find all .toolkitignore files from directory up to root.""" + ignore_files = [] + current_dir = directory + + while current_dir != current_dir.parent: + ignore_file = current_dir / filename + if ignore_file.exists(): + ignore_files.append(ignore_file) + current_dir = current_dir.parent + + return ignore_files + + +def create_ignore_parser_for_module(module_dir: Path) -> ToolkitIgnoreParser: + """Create an ignore parser for a specific module directory.""" + return ToolkitIgnoreParser.from_directory(module_dir) \ No newline at end of file diff --git a/tests/test_unit/test_cdf_tk/test_data_classes/test_module_toolkitignore.py b/tests/test_unit/test_cdf_tk/test_data_classes/test_module_toolkitignore.py new file mode 100644 index 0000000000..be4e2b4de2 --- /dev/null +++ b/tests/test_unit/test_cdf_tk/test_data_classes/test_module_toolkitignore.py @@ -0,0 +1,315 @@ +import tempfile +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +from cognite_toolkit._cdf_tk.data_classes._module_directories import ModuleLocation +from cognite_toolkit._cdf_tk.data_classes._module_toml import ModuleToml +from cognite_toolkit._cdf_tk.feature_flags import Flags + + +class TestModuleLocationWithToolkitIgnore: + """Test ModuleLocation integration with .toolkitignore functionality.""" + + @patch.object(Flags.TOOLKIT_IGNORE, 'is_enabled', return_value=True) + def test_module_ignores_directories_from_toolkitignore(self, mock_flag: Mock) -> None: + """Test that ModuleLocation respects .toolkitignore patterns when feature flag is enabled.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create module directory structure + module_dir = temp_path / "test_module" + module_dir.mkdir() + + # Create .toolkitignore file + ignore_file = module_dir / ".toolkitignore" + ignore_file.write_text("node_modules\n*.tmp\ntemp_files/\n") + + # Create directories - some should be ignored + (module_dir / "auth").mkdir() # Valid resource directory + (module_dir / "data_models").mkdir() # Valid resource directory + (module_dir / "node_modules").mkdir() # Should be ignored + (module_dir / "temp_files").mkdir() # Should be ignored + (module_dir / "custom_folder").mkdir() # Should be flagged as invalid + + # Create some files + (module_dir / "auth" / "groups.yaml").touch() + (module_dir / "data_models" / "model.yaml").touch() + (module_dir / "node_modules" / "package.json").touch() + (module_dir / "temp_files" / "cache.txt").touch() + (module_dir / "custom_folder" / "file.txt").touch() + (module_dir / "temp.tmp").touch() # Should be ignored + + # Create source_paths list (simulating file discovery) + source_paths = [] + for file_path in module_dir.rglob("*"): + if file_path.is_file(): + source_paths.append(file_path) + + # Create ModuleLocation instance + module_location = ModuleLocation( + dir=module_dir, + source_absolute_path=temp_path, + source_paths=source_paths, + is_selected=True, + definition=None + ) + + # Test that ignored directories are not flagged as invalid + not_resource_dirs = module_location.not_resource_directories + + # Should only flag custom_folder as invalid (not ignored directories) + assert "custom_folder" in not_resource_dirs + assert "node_modules" not in not_resource_dirs # Should be ignored + assert "temp_files" not in not_resource_dirs # Should be ignored + assert "auth" not in not_resource_dirs # Valid resource directory + assert "data_models" not in not_resource_dirs # Valid resource directory + + @patch.object(Flags.TOOLKIT_IGNORE, 'is_enabled', return_value=True) + def test_module_with_nested_toolkitignore_files(self, mock_flag: Mock) -> None: + """Test ModuleLocation with nested .toolkitignore files when feature flag is enabled.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create root .toolkitignore + root_ignore = temp_path / ".toolkitignore" + root_ignore.write_text("*.log\n__pycache__\n") + + # Create module directory + module_dir = temp_path / "modules" / "test_module" + module_dir.mkdir(parents=True) + + # Create module .toolkitignore + module_ignore = module_dir / ".toolkitignore" + module_ignore.write_text("temp_data\n*.tmp\n") + + # Create directories + (module_dir / "auth").mkdir() + (module_dir / "data_models").mkdir() + (module_dir / "temp_data").mkdir() # Should be ignored by module ignore + (module_dir / "__pycache__").mkdir() # Should be ignored by root ignore + (module_dir / "invalid_dir").mkdir() # Should be flagged as invalid + + # Create files + (module_dir / "auth" / "groups.yaml").touch() + (module_dir / "data_models" / "model.yaml").touch() + (module_dir / "temp_data" / "cache.txt").touch() + (module_dir / "__pycache__" / "cache.pyc").touch() + (module_dir / "invalid_dir" / "file.txt").touch() + (module_dir / "app.log").touch() # Should be ignored by root ignore + (module_dir / "temp.tmp").touch() # Should be ignored by module ignore + + # Create source_paths list + source_paths = [] + for file_path in module_dir.rglob("*"): + if file_path.is_file(): + source_paths.append(file_path) + + # Create ModuleLocation instance + module_location = ModuleLocation( + dir=module_dir, + source_absolute_path=temp_path, + source_paths=source_paths, + is_selected=True, + definition=None + ) + + # Test that ignored directories are not flagged as invalid + not_resource_dirs = module_location.not_resource_directories + + # Should only flag invalid_dir as invalid + assert "invalid_dir" in not_resource_dirs + assert "temp_data" not in not_resource_dirs # Ignored by module ignore + assert "__pycache__" not in not_resource_dirs # Ignored by root ignore + assert "auth" not in not_resource_dirs # Valid resource directory + assert "data_models" not in not_resource_dirs # Valid resource directory + + @patch.object(Flags.TOOLKIT_IGNORE, 'is_enabled', return_value=True) + def test_module_with_negation_patterns(self, mock_flag: Mock) -> None: + """Test ModuleLocation with negation patterns in .toolkitignore when feature flag is enabled.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create module directory + module_dir = temp_path / "test_module" + module_dir.mkdir() + + # Create .toolkitignore with negation + ignore_file = module_dir / ".toolkitignore" + ignore_file.write_text("temp_*\n!temp_important\n") + + # Create directories + (module_dir / "auth").mkdir() + (module_dir / "temp_cache").mkdir() # Should be ignored + (module_dir / "temp_important").mkdir() # Should NOT be ignored (negation) + (module_dir / "temp_logs").mkdir() # Should be ignored + (module_dir / "other_invalid").mkdir() # Should be flagged as invalid + + # Create files + (module_dir / "auth" / "groups.yaml").touch() + (module_dir / "temp_cache" / "cache.txt").touch() + (module_dir / "temp_important" / "important.txt").touch() + (module_dir / "temp_logs" / "log.txt").touch() + (module_dir / "other_invalid" / "file.txt").touch() + + # Create source_paths list + source_paths = [] + for file_path in module_dir.rglob("*"): + if file_path.is_file(): + source_paths.append(file_path) + + # Create ModuleLocation instance + module_location = ModuleLocation( + dir=module_dir, + source_absolute_path=temp_path, + source_paths=source_paths, + is_selected=True, + definition=None + ) + + # Test that ignored directories are not flagged as invalid + not_resource_dirs = module_location.not_resource_directories + + # Should flag temp_important and other_invalid as invalid + assert "temp_important" in not_resource_dirs # Not ignored due to negation + assert "other_invalid" in not_resource_dirs # Not ignored, so flagged as invalid + assert "temp_cache" not in not_resource_dirs # Ignored by pattern + assert "temp_logs" not in not_resource_dirs # Ignored by pattern + assert "auth" not in not_resource_dirs # Valid resource directory + + @patch.object(Flags.TOOLKIT_IGNORE, 'is_enabled', return_value=False) + def test_module_with_feature_flag_disabled(self, mock_flag: Mock) -> None: + """Test ModuleLocation behavior when .toolkitignore feature flag is disabled.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create module directory structure + module_dir = temp_path / "test_module" + module_dir.mkdir() + + # Create .toolkitignore file (should be ignored when flag is off) + ignore_file = module_dir / ".toolkitignore" + ignore_file.write_text("node_modules\ntemp_files/\n") + + # Create directories + (module_dir / "auth").mkdir() + (module_dir / "data_models").mkdir() + (module_dir / "node_modules").mkdir() # Should be flagged as invalid when flag is off + (module_dir / "temp_files").mkdir() # Should be flagged as invalid when flag is off + + # Create files + (module_dir / "auth" / "groups.yaml").touch() + (module_dir / "data_models" / "model.yaml").touch() + (module_dir / "node_modules" / "package.json").touch() + (module_dir / "temp_files" / "cache.txt").touch() + + # Create source_paths list + source_paths = [] + for file_path in module_dir.rglob("*"): + if file_path.is_file(): + source_paths.append(file_path) + + # Create ModuleLocation instance + module_location = ModuleLocation( + dir=module_dir, + source_absolute_path=temp_path, + source_paths=source_paths, + is_selected=True, + definition=None + ) + + # Test that all non-resource directories are flagged as invalid when flag is off + not_resource_dirs = module_location.not_resource_directories + + # Should flag both directories as invalid since .toolkitignore is disabled + assert "node_modules" in not_resource_dirs + assert "temp_files" in not_resource_dirs + assert "auth" not in not_resource_dirs # Valid resource directory + assert "data_models" not in not_resource_dirs # Valid resource directory + + def test_module_without_toolkitignore(self) -> None: + """Test ModuleLocation behavior when no .toolkitignore file exists.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create module directory structure without .toolkitignore + module_dir = temp_path / "test_module" + module_dir.mkdir() + + # Create directories + (module_dir / "auth").mkdir() + (module_dir / "data_models").mkdir() + (module_dir / "invalid_dir").mkdir() + + # Create files + (module_dir / "auth" / "groups.yaml").touch() + (module_dir / "data_models" / "model.yaml").touch() + (module_dir / "invalid_dir" / "file.txt").touch() + + # Create source_paths list + source_paths = [] + for file_path in module_dir.rglob("*"): + if file_path.is_file(): + source_paths.append(file_path) + + # Create ModuleLocation instance + module_location = ModuleLocation( + dir=module_dir, + source_absolute_path=temp_path, + source_paths=source_paths, + is_selected=True, + definition=None + ) + + # Test that all non-resource directories are flagged as invalid + not_resource_dirs = module_location.not_resource_directories + + # Should flag invalid_dir as invalid (no ignore file to prevent this) + assert "invalid_dir" in not_resource_dirs + assert "auth" not in not_resource_dirs # Valid resource directory + assert "data_models" not in not_resource_dirs # Valid resource directory + + @patch.object(Flags.TOOLKIT_IGNORE, 'is_enabled', return_value=True) + def test_empty_toolkitignore_file(self, mock_flag: Mock) -> None: + """Test ModuleLocation with empty .toolkitignore file when feature flag is enabled.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create module directory + module_dir = temp_path / "test_module" + module_dir.mkdir() + + # Create empty .toolkitignore file + ignore_file = module_dir / ".toolkitignore" + ignore_file.write_text("") + + # Create directories + (module_dir / "auth").mkdir() + (module_dir / "invalid_dir").mkdir() + + # Create files + (module_dir / "auth" / "groups.yaml").touch() + (module_dir / "invalid_dir" / "file.txt").touch() + + # Create source_paths list + source_paths = [] + for file_path in module_dir.rglob("*"): + if file_path.is_file(): + source_paths.append(file_path) + + # Create ModuleLocation instance + module_location = ModuleLocation( + dir=module_dir, + source_absolute_path=temp_path, + source_paths=source_paths, + is_selected=True, + definition=None + ) + + # Test that empty ignore file doesn't prevent flagging + not_resource_dirs = module_location.not_resource_directories + + # Should still flag invalid_dir as invalid + assert "invalid_dir" in not_resource_dirs + assert "auth" not in not_resource_dirs # Valid resource directory \ No newline at end of file diff --git a/tests/test_unit/test_cdf_tk/test_utils/test_ignore_patterns.py b/tests/test_unit/test_cdf_tk/test_utils/test_ignore_patterns.py new file mode 100644 index 0000000000..55793b5d98 --- /dev/null +++ b/tests/test_unit/test_cdf_tk/test_utils/test_ignore_patterns.py @@ -0,0 +1,113 @@ +import tempfile +from pathlib import Path + +import pytest + +from cognite_toolkit._cdf_tk.utils.ignore_patterns import ( + ToolkitIgnoreParser, + ToolkitIgnorePattern, + create_ignore_parser_for_module, + find_toolkitignore_files, +) + + +class TestToolkitIgnorePattern: + """Test the ToolkitIgnorePattern class.""" + + def test_simple_pattern_matching(self): + """Test basic pattern matching.""" + pattern = ToolkitIgnorePattern("node_modules") + assert pattern.matches(Path("node_modules"), is_directory=True) + assert pattern.matches(Path("some/path/node_modules"), is_directory=True) + assert not pattern.matches(Path("node_modules_backup"), is_directory=True) + + def test_wildcard_pattern_matching(self): + """Test wildcard pattern matching.""" + pattern = ToolkitIgnorePattern("*.tmp") + assert pattern.matches(Path("temp.tmp"), is_directory=False) + assert pattern.matches(Path("some/path/temp.tmp"), is_directory=False) + assert not pattern.matches(Path("temp.txt"), is_directory=False) + + def test_directory_only_pattern(self): + """Test directory-only patterns ending with /.""" + pattern = ToolkitIgnorePattern("build/") + assert pattern.matches(Path("build"), is_directory=True) + assert not pattern.matches(Path("build"), is_directory=False) + assert not pattern.matches(Path("build.txt"), is_directory=False) + + def test_negation_pattern(self): + """Test negation patterns starting with !.""" + pattern = ToolkitIgnorePattern("!important.txt") + assert pattern.is_negation + assert pattern.matches(Path("important.txt"), is_directory=False) + + +class TestToolkitIgnoreParser: + """Test the ToolkitIgnoreParser class.""" + + def test_empty_parser(self): + """Test empty parser behavior.""" + parser = ToolkitIgnoreParser() + assert not parser.is_ignored(Path("anything"), is_directory=True) + + def test_single_pattern_ignore(self): + """Test ignoring with a single pattern.""" + pattern = ToolkitIgnorePattern("node_modules") + parser = ToolkitIgnoreParser([pattern]) + + assert parser.is_ignored(Path("node_modules"), is_directory=True) + assert not parser.is_ignored(Path("src"), is_directory=True) + + def test_negation_pattern_override(self): + """Test that negation patterns override previous ignore patterns.""" + patterns = [ + ToolkitIgnorePattern("*.tmp"), + ToolkitIgnorePattern("!important.tmp") + ] + parser = ToolkitIgnoreParser(patterns) + + assert parser.is_ignored(Path("temp.tmp"), is_directory=False) + assert not parser.is_ignored(Path("important.tmp"), is_directory=False) + + def test_from_file_content(self): + """Test creating parser from file content.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.toolkitignore', delete=False) as f: + f.write("# This is a comment\n") + f.write("node_modules\n") + f.write("*.tmp\n") + f.write("!important.tmp\n") + f.write("\n") # Empty line + f.write("build/\n") + f.flush() + + parser = ToolkitIgnoreParser.from_file(Path(f.name)) + + assert len(parser.patterns) == 4 # Excluding comment and empty line + assert parser.is_ignored(Path("node_modules"), is_directory=True) + assert parser.is_ignored(Path("temp.tmp"), is_directory=False) + assert not parser.is_ignored(Path("important.tmp"), is_directory=False) + assert parser.is_ignored(Path("build"), is_directory=True) + + def test_from_nonexistent_file(self): + """Test creating parser from non-existent file.""" + parser = ToolkitIgnoreParser.from_file(Path("nonexistent.toolkitignore")) + assert len(parser.patterns) == 0 + + +class TestIntegrationFunctions: + """Test the integration functions.""" + + def test_create_ignore_parser_for_module(self): + """Test creating ignore parser for a module.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create a .toolkitignore file + ignore_file = temp_path / ".toolkitignore" + ignore_file.write_text("node_modules\n*.tmp\n") + + parser = create_ignore_parser_for_module(temp_path) + + assert len(parser.patterns) == 2 + assert parser.is_ignored(Path("node_modules"), is_directory=True) + assert parser.is_ignored(Path("temp.tmp"), is_directory=False) \ No newline at end of file