Coverage for src/pylint_sort_functions/utils/file_patterns.py: 100%

48 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-08-12 16:06 +0200

1"""File pattern matching utilities for Python project analysis. 

2 

3This module provides utilities for finding Python files, detecting test files, 

4and matching file patterns using glob patterns. 

5""" 

6 

7import fnmatch 

8from pathlib import Path 

9from typing import Any 

10 

11 

12def find_python_files(root_path: Path) -> list[Path]: # pylint: disable=function-should-be-private 

13 """Find all Python files in a project directory. 

14 

15 Recursively searches for files with .py extension while skipping common 

16 directories that should not be analyzed (build artifacts, virtual environments, 

17 caches, etc.). 

18 

19 TODO: Make skip_dirs list configurable for project-specific needs. 

20 

21 :param root_path: Root directory to search for Python files 

22 :type root_path: Path 

23 :returns: List of paths to Python files 

24 :rtype: list[Path] 

25 """ 

26 python_files = [] 

27 

28 # Directories to skip 

29 skip_dirs = { 

30 "__pycache__", 

31 ".git", 

32 ".tox", 

33 ".pytest_cache", 

34 ".mypy_cache", 

35 "venv", 

36 ".venv", 

37 "env", 

38 ".env", 

39 "build", 

40 "dist", 

41 "*.egg-info", 

42 "node_modules", 

43 } 

44 

45 for item in root_path.rglob("*.py"): 

46 # Skip if any parent directory should be skipped 

47 if any(skip_dir in item.parts for skip_dir in skip_dirs): 

48 continue 

49 

50 python_files.append(item) 

51 

52 return python_files 

53 

54 

55def is_unittest_file( # pylint: disable=function-should-be-private,too-many-return-statements,too-many-branches 

56 module_name: str, privacy_config: dict[str, Any] | None = None 

57) -> bool: 

58 """Check if a module name indicates a unit test file. 

59 

60 Detects test files based on configurable patterns and built-in heuristics. 

61 Can be configured to override built-in detection or add additional patterns. 

62 

63 Built-in detection patterns: 

64 - Files in 'tests' or 'test' directories 

65 - Files starting with 'test_' 

66 - Files ending with '_test' 

67 - conftest.py files (pytest configuration) 

68 - Files containing 'test' in their path components 

69 

70 :param module_name: The module name to check (e.g., 'package.tests.test_utils') 

71 :type module_name: str 

72 :param privacy_config: Privacy configuration with exclusion patterns 

73 :type privacy_config: dict[str, Any] | None 

74 :returns: True if module appears to be a test file 

75 :rtype: bool 

76 """ 

77 if privacy_config is None: 

78 privacy_config = {} 

79 

80 # Get configuration options 

81 exclude_dirs = privacy_config.get("exclude_dirs", []) 

82 exclude_patterns = privacy_config.get("exclude_patterns", []) 

83 additional_test_patterns = privacy_config.get("additional_test_patterns", []) 

84 override_test_detection = privacy_config.get("override_test_detection", False) 

85 

86 # Check directory exclusions first 

87 lower_name = module_name.lower() 

88 parts = lower_name.split(".") 

89 

90 # Check if file is in an excluded directory 

91 for exclude_dir in exclude_dirs: 

92 if exclude_dir.lower() in parts: 

93 return True 

94 

95 # Check file pattern exclusions 

96 for pattern in exclude_patterns: 

97 if _matches_file_pattern(module_name, pattern): 

98 return True 

99 

100 # Check additional test patterns 

101 for pattern in additional_test_patterns: 

102 if _matches_file_pattern(module_name, pattern): 

103 return True 

104 

105 # If override is enabled, only use configured patterns 

106 if override_test_detection: 

107 return False 

108 

109 # Built-in test detection (original logic) 

110 # Check if any directory in the path is a test directory 

111 if "tests" in parts or "test" in parts: 

112 return True 

113 

114 # Get the file name (last component) 

115 if parts: 

116 filename = parts[-1] 

117 

118 # Check for common test file patterns 

119 if filename.startswith("test_"): 

120 return True 

121 if filename.endswith("_test"): 

122 return True 

123 if filename == "conftest": # pytest configuration file 

124 return True 

125 

126 # Fallback: check if 'test' appears anywhere (catches edge cases) 

127 # This is more permissive but ensures we do not miss test files 

128 return "test" in lower_name 

129 

130 

131def _matches_file_pattern(module_name: str, pattern: str) -> bool: 

132 """Check if a module name matches a file pattern. 

133 

134 Supports glob patterns for matching file names and paths. 

135 

136 :param module_name: Module name to check (e.g., 'package.tests.test_utils') 

137 :type module_name: str 

138 :param pattern: Glob pattern to match against (e.g., 'test_*.py', '*_test.py') 

139 :type pattern: str 

140 :returns: True if module name matches pattern 

141 :rtype: bool 

142 """ 

143 # Convert module name to filename-like format for pattern matching 

144 # e.g., "package.tests.test_utils" -> "package/tests/test_utils.py" 

145 file_path = module_name.replace(".", "/") + ".py" 

146 

147 # Also check just the module name for direct matching 

148 parts = module_name.split(".") 

149 if parts: 

150 filename = parts[-1] + ".py" # Add .py for pattern matching 

151 

152 # Check both full path and just filename 

153 return fnmatch.fnmatch(file_path, pattern) or fnmatch.fnmatch(filename, pattern) 

154 

155 return fnmatch.fnmatch(file_path, pattern) # pragma: no cover