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
« 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.
3This module provides file discovery, path validation, and working directory computation.
4"""
6from __future__ import annotations
8import os
9import sys
11from loguru import logger
12from rich.progress import Progress, SpinnerColumn, TextColumn
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
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]
36def setup_exclude_patterns(
37 exclude_patterns: list[str],
38) -> list[str]:
39 """Set up exclude patterns with defaults and .lintro-ignore.
41 Args:
42 exclude_patterns: Current exclude patterns to extend.
44 Returns:
45 Updated list of exclude patterns.
46 """
47 patterns = list(exclude_patterns)
49 # Add default exclude patterns
50 for pattern in DEFAULT_EXCLUDE_PATTERNS:
51 if pattern not in patterns:
52 patterns.append(pattern)
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}")
68 return patterns
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.
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.
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()
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")
108 logger.debug(
109 f"File discovery: {len(files)} files matching {definition.file_patterns}",
110 )
111 return files
114def validate_paths(paths: list[str]) -> None:
115 """Validate that paths exist and are accessible.
117 Args:
118 paths: Paths to validate.
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}")
131def get_cwd(paths: list[str]) -> str | None:
132 """Get common parent directory for paths.
134 Args:
135 paths: Paths to compute common parent for.
137 Returns:
138 Common parent directory path, or None if not applicable.
139 """
140 if not paths:
141 return None
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))
153 if len(parent_dirs) == 1:
154 return parent_dirs.pop()
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