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

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

2

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

4 and matching file patterns using glob patterns.

5"""

6

7 import fnmatch

8 from pathlib import Path

9 from typing import Any

10

11

12 def 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

55 def 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

131 def _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