Coverage for .tox/py313/lib/python3.13/site-packages/pylint_sort_functions/cli.py: 100%

49 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-07 04:45 +0200

1"""Command-line interface for pylint-sort-functions auto-fix. 

2 

3This module provides the standalone CLI tool that users invoke via: 

4 $ pylint-sort-functions [options] <paths> 

5 

6The entry point is configured in pyproject.toml and maps the 'pylint-sort-functions' 

7command to the main() function in this module. This provides auto-fix functionality 

8independent of the PyLint plugin integration. 

9 

10Usage examples: 

11 $ pylint-sort-functions --fix src/ 

12 $ pylint-sort-functions --dry-run --verbose myproject/ 

13 $ pylint-sort-functions --fix --no-backup --ignore-decorators "@app.route" src/ 

14""" 

15 

16# Using argparse (stdlib) instead of click to maintain zero external dependencies. 

17# This lightweight approach is sufficient for our flat argument structure. 

18# Future: If subcommands are added, consider migrating to click for better UX. 

19import argparse 

20import sys 

21from pathlib import Path 

22from typing import List 

23 

24from pylint_sort_functions import auto_fix 

25from pylint_sort_functions.auto_fix import AutoFixConfig # Class - direct import OK 

26 

27# Public functions 

28 

29 

30def main() -> int: # pylint: disable=too-many-return-statements,too-many-branches 

31 """Main CLI entry point for pylint-sort-functions tool. 

32 

33 Provides a complete workflow for auto-fixing function and method sorting: 

34 1. Parse and validate command-line arguments 

35 2. Resolve and validate input paths (files/directories) 

36 3. Discover Python files recursively in directories 

37 4. Configure auto-fix settings from CLI arguments 

38 5. Process files with function/method sorting and comment preservation 

39 6. Report results with optional verbose output 

40 

41 The tool operates in different modes: 

42 - Check-only: Default mode, shows help and exits 

43 - Dry-run: Shows what would be changed without modifying files 

44 - Fix: Actually modifies files with optional backup creation 

45 

46 Exit codes: 

47 - 0: Success (files processed successfully, or check-only mode) 

48 - 1: Error (invalid paths, processing failures, user interruption) 

49 

50 Error handling: 

51 - Provides user-friendly error messages instead of stack traces 

52 - Handles filesystem errors, permission issues, and processing failures 

53 - Graceful handling of keyboard interruption (Ctrl+C) 

54 

55 Side effects: 

56 - May modify Python files when --fix is used 

57 - May create .bak backup files unless --no-backup is specified 

58 - Outputs progress and results to stdout 

59 

60 :returns: Exit code (0 for success, 1 for error) 

61 :rtype: int 

62 """ 

63 parser = argparse.ArgumentParser( 

64 prog="pylint-sort-functions", 

65 description="Auto-fix function and method sorting in Python files", 

66 ) 

67 _add_parser_arguments(parser) 

68 

69 args = parser.parse_args() 

70 

71 # Validate arguments 

72 if not args.fix and not args.dry_run: 

73 print( 

74 "Note: Running in check-only mode. Use --fix or --dry-run to make changes." 

75 ) 

76 print("Use 'pylint-sort-functions --help' for more options.") 

77 return 0 

78 

79 # Convert paths and find Python files 

80 try: 

81 paths = [Path(p).resolve() for p in args.paths] 

82 for path in paths: 

83 if not path.exists(): 

84 print(f"Error: Path does not exist: {path}") 

85 return 1 

86 

87 python_files = _find_python_files(paths) 

88 if not python_files: 

89 print("No Python files found in the specified paths.") 

90 return 0 

91 

92 # Catch broad exceptions for CLI robustness - path operations can fail in 

93 # many OS-specific ways, and we want clean error messages not stacktraces 

94 except Exception as e: # pragma: no cover # pylint: disable=broad-exception-caught 

95 print(f"Error processing paths: {e}") 

96 return 1 

97 

98 # Configure auto-fix 

99 config = AutoFixConfig( 

100 dry_run=args.dry_run, 

101 backup=not args.no_backup, 

102 ignore_decorators=args.ignore_decorators or [], 

103 ) 

104 

105 if args.verbose: # pragma: no cover 

106 print(f"Processing {len(python_files)} Python files...") 

107 if config.ignore_decorators: 

108 print(f"Ignoring decorators: {', '.join(config.ignore_decorators)}") 

109 

110 # Process files 

111 try: 

112 files_processed, files_modified = auto_fix.sort_python_files( 

113 python_files, config 

114 ) 

115 

116 if args.verbose or files_modified > 0: # pragma: no cover 

117 if config.dry_run: 

118 print(f"Would modify {files_modified} of {files_processed} files") 

119 else: 

120 print(f"Modified {files_modified} of {files_processed} files") 

121 if config.backup and files_modified > 0: 

122 print("Backup files created with .bak extension") 

123 

124 return 0 

125 

126 except KeyboardInterrupt: 

127 print("\nOperation cancelled by user.") 

128 return 1 

129 except Exception as e: # pylint: disable=broad-exception-caught 

130 print(f"Error during processing: {e}") 

131 return 1 

132 

133 

134# Private functions 

135 

136 

137def _add_parser_arguments(parser: argparse.ArgumentParser) -> None: 

138 """Configure CLI argument parser with all supported options. 

139 

140 :param parser: The argument parser to configure 

141 :type parser: argparse.ArgumentParser 

142 """ 

143 parser.add_argument( 

144 "paths", nargs="+", type=Path, help="Python files or directories to process" 

145 ) 

146 

147 parser.add_argument( 

148 "--fix", 

149 action="store_true", 

150 help="Apply auto-fix to sort functions (default: check only)", 

151 ) 

152 

153 parser.add_argument( 

154 "--dry-run", 

155 action="store_true", 

156 help="Show what would be changed without modifying files", 

157 ) 

158 

159 parser.add_argument( 

160 "--no-backup", 

161 action="store_true", 

162 help="Do not create backup files (.bak) when fixing", 

163 ) 

164 

165 parser.add_argument( 

166 "--ignore-decorators", 

167 action="append", 

168 metavar="PATTERN", 

169 help='Decorator patterns to ignore (e.g., "@app.route" "@*.command"). ' 

170 + "Can be used multiple times.", 

171 ) 

172 

173 parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") 

174 

175 

176def _find_python_files(paths: List[Path]) -> List[Path]: 

177 """Find all Python files in the given paths. 

178 

179 :param paths: List of file or directory paths 

180 :type paths: List[Path] 

181 :returns: List of Python file paths 

182 :rtype: List[Path] 

183 """ 

184 python_files = [] 

185 

186 for path in paths: 

187 if path.is_file() and path.suffix == ".py": 

188 python_files.append(path) 

189 elif path.is_dir(): 

190 # Recursively find Python files 

191 python_files.extend(path.rglob("*.py")) 

192 

193 return python_files 

194 

195 

196if __name__ == "__main__": # pragma: no cover 

197 sys.exit(main())