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

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

2

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

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

5

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

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

8 independent of the PyLint plugin integration.

9

10 Usage 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.

19 import argparse

20 import sys

21 from pathlib import Path

22 from typing import List

23

24 from pylint_sort_functions import auto_fix

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

26

27# Public functions

28

29

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

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

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

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

197 sys.exit(main())