qgis-plugin-analyzer 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ # /***************************************************************************
2
+ # QGIS Plugin Analyzer
3
+ # A QGIS tool
4
+ # Static code analysis and standards audit for QGIS plugins.
5
+ # -------------------
6
+ # begin : 2025-12-28
7
+ # git sha : $Format:%H$
8
+ # copyright : (C) 2025 by Juan M Bernales
9
+ # email : juanbernales@gmail.com
10
+ # ***************************************************************************/
11
+ #
12
+ # /***************************************************************************
13
+ # * *
14
+ # * This program is free software; you can redistribute it and/or modify *
15
+ # * it under the terms of the GNU General Public License as published by *
16
+ # * the Free Software Foundation; either version 2 of the License, or *
17
+ # * (at your option) any later version. *
18
+ # * *
19
+ # ***************************************************************************/
analyzer/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ # /***************************************************************************
2
+ # QGIS Plugin Analyzer
3
+ # A QGIS tool
4
+ # Static code analysis and standards audit for QGIS plugins.
5
+ # -------------------
6
+ # begin : 2025-12-28
7
+ # git sha : $Format:%H$
8
+ # copyright : (C) 2025 by Juan M Bernales
9
+ # email : juanbernales@gmail.com
10
+ # ***************************************************************************/
11
+ #
12
+ # /***************************************************************************
13
+ # * *
14
+ # * This program is free software; you can redistribute it and/or modify *
15
+ # * it under the terms of the GNU General Public License as published by *
16
+ # * the Free Software Foundation; either version 2 of the License, or *
17
+ # * (at your option) any later version. *
18
+ # * *
19
+ # ***************************************************************************/
analyzer/cli.py ADDED
@@ -0,0 +1,311 @@
1
+ # /***************************************************************************
2
+ # QGIS Plugin Analyzer
3
+ # A QGIS tool
4
+ # Static code analysis and standards audit for QGIS plugins.
5
+ # -------------------
6
+ # begin : 2025-12-28
7
+ # git sha : $Format:%H$
8
+ # copyright : (C) 2025 by Juan M Bernales
9
+ # email : juanbernales@gmail.com
10
+ # ***************************************************************************/
11
+ #
12
+ # /***************************************************************************
13
+ # * *
14
+ # * This program is free software; you can redistribute it and/or modify *
15
+ # * it under the terms of the GNU General Public License as published by *
16
+ # * the Free Software Foundation; either version 2 of the License, or *
17
+ # * (at your option) any later version. *
18
+ # * *
19
+ # ***************************************************************************/
20
+
21
+
22
+ import argparse
23
+ import pathlib
24
+ import sys
25
+
26
+ from .engine import ProjectAnalyzer
27
+ from .utils import logger, setup_logger
28
+
29
+
30
+ def _setup_argument_parser() -> argparse.ArgumentParser:
31
+ """Sets up and returns the argument parser with all subcommands.
32
+
33
+ Returns:
34
+ A configured ArgumentParser instance.
35
+ """
36
+ parser = argparse.ArgumentParser(
37
+ description="QGIS Plugin Analyzer - A guardian for your PyQGIS code"
38
+ )
39
+ subparsers = parser.add_subparsers(dest="command", help="Command to execute")
40
+
41
+ # Analyze Command
42
+ analyze_parser = subparsers.add_parser("analyze", help="Analyze an existing QGIS plugin")
43
+ analyze_parser.add_argument("project_path", help="Path to the QGIS project to analyze")
44
+ analyze_parser.add_argument(
45
+ "-o",
46
+ "--output",
47
+ help="Output directory for reports",
48
+ default="./analysis_results",
49
+ )
50
+ analyze_parser.add_argument(
51
+ "-r",
52
+ "--report",
53
+ action="store_true",
54
+ help="Generate detailed HTML/Markdown reports",
55
+ )
56
+ analyze_parser.add_argument(
57
+ "-p",
58
+ "--profile",
59
+ help="Configuration profile from pyproject.toml",
60
+ default="default",
61
+ )
62
+
63
+ # Fix Command
64
+ fix_parser = subparsers.add_parser("fix", help="Auto-fix common QGIS plugin issues")
65
+ fix_parser.add_argument("path", type=str, help="Path to the QGIS plugin directory")
66
+ fix_parser.add_argument(
67
+ "--dry-run",
68
+ action="store_true",
69
+ default=True,
70
+ help="Show proposed changes without applying (default: True)",
71
+ )
72
+ fix_parser.add_argument("--apply", action="store_true", help="Apply fixes (disables dry-run)")
73
+ fix_parser.add_argument(
74
+ "--auto-approve",
75
+ action="store_true",
76
+ help="Apply all fixes without confirmation",
77
+ )
78
+ fix_parser.add_argument(
79
+ "-p",
80
+ "--profile",
81
+ help="Configuration profile from pyproject.toml",
82
+ default="default",
83
+ )
84
+ fix_parser.add_argument(
85
+ "--rules",
86
+ type=str,
87
+ help="Comma-separated list of rule IDs to fix",
88
+ )
89
+
90
+ # List Rules Command
91
+ subparsers.add_parser("list-rules", help="List all available QGIS audit rules")
92
+
93
+ # Init Command
94
+ subparsers.add_parser("init", help="Initialize a new .analyzerignore with defaults")
95
+
96
+ # Summary Command
97
+ summary_parser = subparsers.add_parser(
98
+ "summary", help="Show a quick terminal summary of analysis results"
99
+ )
100
+ summary_parser.add_argument(
101
+ "-i",
102
+ "--input",
103
+ help="Path to the research JSON file",
104
+ default="analysis_results/project_context.json",
105
+ )
106
+ summary_parser.add_argument(
107
+ "-b",
108
+ "--by",
109
+ choices=["total", "modules", "functions", "classes"],
110
+ default="total",
111
+ help="Granularity of the summary (default: total)",
112
+ )
113
+
114
+ return parser
115
+
116
+
117
+ def _handle_fix_command(args: argparse.Namespace) -> bool:
118
+ """Handles the execution of the 'fix' command.
119
+
120
+ Args:
121
+ args: Parsed command line arguments.
122
+
123
+ Returns:
124
+ True if the fix process completed successfully, False otherwise.
125
+ """
126
+ import json
127
+
128
+ from .fixer import AutoFixer
129
+
130
+ project_path = pathlib.Path(args.path).resolve()
131
+ if not project_path.exists():
132
+ print(f"❌ Path not found: {project_path}")
133
+ return False
134
+
135
+ # Run analysis first
136
+ print("🔍 Analyzing project for fixable issues...")
137
+ analyzer = ProjectAnalyzer(
138
+ str(project_path),
139
+ args.output if hasattr(args, "output") else "./analysis_results",
140
+ args.profile if hasattr(args, "profile") else "default",
141
+ )
142
+ analyzer.run()
143
+
144
+ # Load issues
145
+ context_file = analyzer.output_dir / "project_context.json"
146
+ with open(context_file) as f:
147
+ context = json.load(f)
148
+
149
+ all_issues = []
150
+ for module in context.get("modules", []):
151
+ all_issues.extend(module.get("ast_issues", []))
152
+
153
+ if args.rules:
154
+ rule_ids = [r.strip() for r in args.rules.split(",")]
155
+ all_issues = [i for i in all_issues if i.get("type") in rule_ids]
156
+
157
+ fixer = AutoFixer(project_path, dry_run=not args.apply)
158
+ fixable = fixer.get_fixable_issues(all_issues)
159
+
160
+ if not fixable:
161
+ print("✅ No fixable issues found!")
162
+ return True
163
+
164
+ print(f"\n📋 Found {len(fixable)} fixable issue(s)")
165
+ if not args.apply:
166
+ print("\n⚠️ DRY RUN MODE (use --apply to execute changes)\n")
167
+
168
+ stats = fixer.apply_fixes(fixable, interactive=not args.auto_approve)
169
+ print(
170
+ f"\n📊 Summary: Applied: {stats['applied']}, Skipped: {stats['skipped']}, Failed: {stats['failed']}"
171
+ )
172
+ return True
173
+
174
+
175
+ def _handle_analyze_command(args: argparse.Namespace) -> None:
176
+ """Handles the execution of the 'analyze' command.
177
+
178
+ Args:
179
+ args: Parsed command line arguments.
180
+ """
181
+ # Force generate_html based on flag, overriding profile if necessary for CLI usage
182
+ # We pass it via a temporary config override or modify the analyzer init
183
+ # For now, let's pass it to the analyzer constructor or modify config after init
184
+
185
+ analyzer = ProjectAnalyzer(args.project_path, args.output, args.profile)
186
+
187
+ # Override config based on CLI flag
188
+ if hasattr(args, "report") and args.report:
189
+ analyzer.config["generate_html"] = True
190
+ else:
191
+ analyzer.config["generate_html"] = False
192
+
193
+ success = analyzer.run()
194
+
195
+ # Always show terminal summary
196
+ from .reporters.summary_reporter import report_summary
197
+
198
+ # If we didn't generate reports, we might still want to show the summary
199
+ # using the in-memory data or the context file if it was saved.
200
+ # Engine saves json context by default? Let's check engine.py.
201
+ # Assuming engine saves project_context.json always or we need to access results directly.
202
+ # To keep it simple, we depend on the engine saving the context or returning it.
203
+ # Current engine.run retuns bool.
204
+
205
+ context_path = analyzer.output_dir / "project_context.json"
206
+ if context_path.exists():
207
+ report_summary(context_path)
208
+
209
+ if not success:
210
+ sys.exit(1)
211
+
212
+
213
+ def _handle_list_rules_command() -> None:
214
+ """Handles the 'list-rules' command by displaying available audit rules."""
215
+ from .rules import get_qgis_audit_rules
216
+
217
+ rules = get_qgis_audit_rules()
218
+ print("\n📋 QGIS Audit Rules Catalog:")
219
+ print("=" * 30)
220
+ for r in rules:
221
+ print(f"- [{r['severity'].upper()}] {r['id']}: {r['message']}")
222
+ print(f"\nTotal: {len(rules)} rules.\n")
223
+
224
+
225
+ def _handle_init_command() -> None:
226
+ """Handles the 'init' command by creating a default .analyzerignore file."""
227
+ from .utils import DEFAULT_EXCLUDE
228
+
229
+ ignore_file = pathlib.Path(".analyzerignore")
230
+ if ignore_file.exists():
231
+ print("⚠️ .analyzerignore already exists. Skipping.")
232
+ else:
233
+ with open(ignore_file, "w") as f:
234
+ f.write("# QGIS Plugin Analyzer Ignore File\n")
235
+ for p in DEFAULT_EXCLUDE:
236
+ f.write(f"{p}\n")
237
+ print("✅ Created .analyzerignore with default excludes.")
238
+
239
+
240
+ def _handle_summary_command(args: argparse.Namespace) -> None:
241
+ """Handles the 'summary' command by displaying a terminal report.
242
+
243
+ Args:
244
+ args: Parsed command line arguments.
245
+ """
246
+ from .reporters.summary_reporter import report_summary
247
+
248
+ input_path = pathlib.Path(args.input).resolve()
249
+ report_summary(input_path, by=args.by)
250
+
251
+
252
+ def main() -> None:
253
+ """Main entry point for the QGIS Plugin Analyzer CLI.
254
+
255
+ Orchestrates the command execution based on parsed arguments and
256
+ sets up the global logging environment.
257
+ """
258
+ parser = _setup_argument_parser()
259
+
260
+ # Legacy support / default to analyze if no command provided
261
+ if len(sys.argv) > 1 and sys.argv[1] not in [
262
+ "analyze",
263
+ "fix",
264
+ "list-rules",
265
+ "init",
266
+ "summary",
267
+ "-h",
268
+ "--help",
269
+ ]:
270
+ # If the first argument is a path (doesn't start with -), assume 'analyze'
271
+ if not sys.argv[1].startswith("-"):
272
+ sys.argv.insert(1, "analyze")
273
+
274
+ args = parser.parse_args()
275
+
276
+ # Initialize logger (default to analysis_results if not specified)
277
+ output_dir = pathlib.Path(getattr(args, "output", "./analysis_results")).resolve()
278
+ output_dir.mkdir(parents=True, exist_ok=True)
279
+ setup_logger(output_dir)
280
+
281
+ try:
282
+ if args.command == "fix":
283
+ _handle_fix_command(args)
284
+ elif args.command == "analyze":
285
+ _handle_analyze_command(args)
286
+ elif args.command == "list-rules":
287
+ _handle_list_rules_command()
288
+ elif args.command == "init":
289
+ _handle_init_command()
290
+ elif args.command == "summary":
291
+ _handle_summary_command(args)
292
+ else:
293
+ parser.print_help()
294
+
295
+ except KeyboardInterrupt:
296
+ logger.info("\n⏹️ Analysis interrupted.")
297
+ sys.exit(1)
298
+ except FileNotFoundError as e:
299
+ logger.error(f"Error: File not found: {e}")
300
+ sys.exit(1)
301
+ except ValueError as e:
302
+ # This handles path traversal or other validation errors
303
+ logger.error(f"Error: {e}")
304
+ sys.exit(1)
305
+ except Exception as e:
306
+ logger.critical(f"Critical Error: {e}", exc_info=True)
307
+ sys.exit(1)
308
+
309
+
310
+ if __name__ == "__main__":
311
+ main()