Coverage for lintro / plugins / file_discovery.py: 95%

56 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""File discovery and path utilities for tool plugins. 

2 

3This module provides file discovery, path validation, and working directory computation. 

4""" 

5 

6from __future__ import annotations 

7 

8import os 

9import sys 

10 

11from loguru import logger 

12from rich.progress import Progress, SpinnerColumn, TextColumn 

13 

14from lintro.plugins.protocol import ToolDefinition 

15from lintro.utils.path_filtering import walk_files_with_excludes 

16from lintro.utils.path_utils import find_lintro_ignore 

17 

18# Default exclude patterns for file discovery 

19DEFAULT_EXCLUDE_PATTERNS: list[str] = [ 

20 ".git", 

21 ".hg", 

22 ".svn", 

23 "__pycache__", 

24 "*.pyc", 

25 "*.pyo", 

26 "*.pyd", 

27 "*cache*", 

28 ".coverage", 

29 "htmlcov", 

30 "dist", 

31 "build", 

32 "*.egg-info", 

33] 

34 

35 

36def setup_exclude_patterns( 

37 exclude_patterns: list[str], 

38) -> list[str]: 

39 """Set up exclude patterns with defaults and .lintro-ignore. 

40 

41 Args: 

42 exclude_patterns: Current exclude patterns to extend. 

43 

44 Returns: 

45 Updated list of exclude patterns. 

46 """ 

47 patterns = list(exclude_patterns) 

48 

49 # Add default exclude patterns 

50 for pattern in DEFAULT_EXCLUDE_PATTERNS: 

51 if pattern not in patterns: 

52 patterns.append(pattern) 

53 

54 # Add .lintro-ignore patterns if present 

55 try: 

56 lintro_ignore_path = find_lintro_ignore() 

57 if lintro_ignore_path and lintro_ignore_path.exists(): 

58 with open(lintro_ignore_path, encoding="utf-8") as f: 

59 for line in f: 

60 line_stripped = line.strip() 

61 if not line_stripped or line_stripped.startswith("#"): 

62 continue 

63 if line_stripped not in patterns: 

64 patterns.append(line_stripped) 

65 except (OSError, UnicodeDecodeError) as e: 

66 logger.debug(f"Could not read .lintro-ignore: {e}") 

67 

68 return patterns 

69 

70 

71def discover_files( 

72 paths: list[str], 

73 definition: ToolDefinition, 

74 exclude_patterns: list[str], 

75 include_venv: bool = False, 

76 show_progress: bool = True, 

77) -> list[str]: 

78 """Discover files matching the tool's patterns. 

79 

80 Args: 

81 paths: Input paths to search. 

82 definition: Tool definition with file patterns. 

83 exclude_patterns: Patterns to exclude. 

84 include_venv: Whether to include virtual environment files. 

85 show_progress: Whether to show a progress spinner during discovery. 

86 

87 Returns: 

88 List of matching file paths. 

89 """ 

90 # Disable progress when not in a TTY or when show_progress is False 

91 disable_progress = not show_progress or not sys.stdout.isatty() 

92 

93 with Progress( 

94 SpinnerColumn(), 

95 TextColumn("[progress.description]{task.description}"), 

96 transient=True, 

97 disable=disable_progress, 

98 ) as progress: 

99 task = progress.add_task("Discovering files...", total=None) 

100 files = walk_files_with_excludes( 

101 paths=paths, 

102 file_patterns=definition.file_patterns, 

103 exclude_patterns=exclude_patterns, 

104 include_venv=include_venv, 

105 ) 

106 progress.update(task, description=f"Found {len(files)} files") 

107 

108 logger.debug( 

109 f"File discovery: {len(files)} files matching {definition.file_patterns}", 

110 ) 

111 return files 

112 

113 

114def validate_paths(paths: list[str]) -> None: 

115 """Validate that paths exist and are accessible. 

116 

117 Args: 

118 paths: Paths to validate. 

119 

120 Raises: 

121 FileNotFoundError: If any path does not exist. 

122 PermissionError: If any path is not accessible. 

123 """ 

124 for path in paths: 

125 if not os.path.exists(path): 

126 raise FileNotFoundError(f"Path does not exist: {path}") 

127 if not os.access(path, os.R_OK): 

128 raise PermissionError(f"Path is not accessible: {path}") 

129 

130 

131def get_cwd(paths: list[str]) -> str | None: 

132 """Get common parent directory for paths. 

133 

134 Args: 

135 paths: Paths to compute common parent for. 

136 

137 Returns: 

138 Common parent directory path, or None if not applicable. 

139 """ 

140 if not paths: 

141 return None 

142 

143 # Get the parent directory for each path 

144 # For files: use dirname; for directories: use the path itself 

145 parent_dirs: set[str] = set() 

146 for p in paths: 

147 abs_path = os.path.abspath(p) 

148 if os.path.isdir(abs_path): 

149 parent_dirs.add(abs_path) 

150 else: 

151 parent_dirs.add(os.path.dirname(abs_path)) 

152 

153 if len(parent_dirs) == 1: 

154 return parent_dirs.pop() 

155 

156 try: 

157 return os.path.commonpath(list(parent_dirs)) 

158 except ValueError: 

159 # Can happen on Windows with paths on different drives 

160 return None