Coverage for .tox/py311/lib/python3.11/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
« 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.
3This module provides the standalone CLI tool that users invoke via:
4 $ pylint-sort-functions [options] <paths>
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.
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"""
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
24from pylint_sort_functions import auto_fix
25from pylint_sort_functions.auto_fix import AutoFixConfig # Class - direct import OK
27# Public functions
30def main() -> int: # pylint: disable=too-many-return-statements,too-many-branches
31 """Main CLI entry point for pylint-sort-functions tool.
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
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
46 Exit codes:
47 - 0: Success (files processed successfully, or check-only mode)
48 - 1: Error (invalid paths, processing failures, user interruption)
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)
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
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)
69 args = parser.parse_args()
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
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
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
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
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 )
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)}")
110 # Process files
111 try:
112 files_processed, files_modified = auto_fix.sort_python_files(
113 python_files, config
114 )
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")
124 return 0
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
134# Private functions
137def _add_parser_arguments(parser: argparse.ArgumentParser) -> None:
138 """Configure CLI argument parser with all supported options.
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 )
147 parser.add_argument(
148 "--fix",
149 action="store_true",
150 help="Apply auto-fix to sort functions (default: check only)",
151 )
153 parser.add_argument(
154 "--dry-run",
155 action="store_true",
156 help="Show what would be changed without modifying files",
157 )
159 parser.add_argument(
160 "--no-backup",
161 action="store_true",
162 help="Do not create backup files (.bak) when fixing",
163 )
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 )
173 parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
176def _find_python_files(paths: List[Path]) -> List[Path]:
177 """Find all Python files in the given paths.
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 = []
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"))
193 return python_files
196if __name__ == "__main__": # pragma: no cover
197 sys.exit(main())