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.
@@ -0,0 +1,222 @@
1
+ """Summary reporter for terminal-based analysis results.
2
+
3
+ This module provides functions to display a professional summary of the
4
+ analysis findings directly in the terminal, with ANSI color support
5
+ for quality indicators.
6
+ """
7
+
8
+ import json
9
+ import pathlib
10
+ from typing import Any, Dict, List
11
+
12
+
13
+ def print_colored_score(label: str, score: Any) -> None:
14
+ """Prints a score with ANSI colors based on its value.
15
+
16
+ Args:
17
+ label: The label for the score.
18
+ score: The numeric score value (or "N/A").
19
+ """
20
+ if score == "N/A":
21
+ print(f"{label}: \033[90mN/A\033[0m")
22
+ return
23
+
24
+ try:
25
+ val = float(score)
26
+ if val >= 80:
27
+ color = "\033[92m" # Green
28
+ elif val >= 50:
29
+ color = "\033[93m" # Yellow
30
+ else:
31
+ color = "\033[91m" # Red
32
+ print(f"{label}: {color}{val:.1f}/100\033[0m")
33
+ except (ValueError, TypeError):
34
+ print(f"{label}: {score}")
35
+
36
+
37
+ def report_summary(input_path: pathlib.Path, by: str = "total") -> bool:
38
+ """Reads analysis JSON and prints a professional terminal summary.
39
+
40
+ Args:
41
+ input_path: Path to the project_context.json file.
42
+ by: Granularity level ('total', 'modules', 'functions', 'classes').
43
+
44
+ Returns:
45
+ True if the report was successfully generated, False otherwise.
46
+ """
47
+ if not input_path.exists():
48
+ print(f"\033[91mError: Analysis file not found at {input_path}\033[0m")
49
+ return False
50
+
51
+ try:
52
+ with open(input_path, encoding="utf-8") as f:
53
+ data = json.load(f)
54
+
55
+ if by == "total":
56
+ return _report_total(data)
57
+ elif by == "modules":
58
+ return _report_by_modules(data)
59
+ elif by == "functions":
60
+ return _report_by_functions(data)
61
+ elif by == "classes":
62
+ return _report_by_classes(data)
63
+ else:
64
+ print(f"\033[91mError: Unknown summary mode '{by}'\033[0m")
65
+ return False
66
+
67
+ except Exception as e:
68
+ print(f"\033[91mError reading analysis results: {e}\033[0m")
69
+ return False
70
+
71
+
72
+ def _report_total(data: Dict[str, Any]) -> bool:
73
+ """Prints the executive total summary."""
74
+ print("\n\033[1m📋 QGIS Plugin Analyzer: Project Summary\033[0m")
75
+ print("=" * 45)
76
+
77
+ # 1. Quality Indicators
78
+ metrics = data.get("metrics", {})
79
+ print("\n\033[1m📊 Quality Indicators\033[0m")
80
+ print_colored_score("- Module Stability Score", metrics.get("quality_score", "N/A"))
81
+ print_colored_score("- Code Maintainability Score", metrics.get("maintainability_score", "N/A"))
82
+
83
+ # 2. Research Metrics
84
+ research = data.get("research_summary", {})
85
+ if research:
86
+ print("\n\033[1m🔬 Research-based Metrics\033[0m")
87
+ params_cov = research.get("type_hint_coverage", 0)
88
+ returns_cov = research.get("return_hint_coverage", 0)
89
+ doc_cov = research.get("docstring_coverage", 0)
90
+ styles = research.get("detected_docstring_styles", [])
91
+ style = styles[0] if styles else "Unknown"
92
+
93
+ print(f"- Type Hint Coverage (Params): {params_cov:.1f}%")
94
+ print(f"- Type Hint Coverage (Returns): {returns_cov:.1f}%")
95
+ print(f"- Docstring Coverage: {doc_cov:.1f}%")
96
+ print(f"- Documentation Style: {style}")
97
+
98
+ # 3. Issue Summary
99
+ issues: List[Dict[str, Any]] = []
100
+ for module in data.get("modules", []):
101
+ mod_path = module.get("path", "unknown")
102
+ for issue in module.get("ast_issues", []):
103
+ issue["file"] = mod_path
104
+ issues.append(issue)
105
+
106
+ if not issues:
107
+ print("\n\033[92m✅ No issues detected! Your project looks great.\033[0m")
108
+ else:
109
+ print(f"\n\033[1m⚠️ Issue Statistics ({len(issues)} total)\033[0m")
110
+ counts: Dict[str, int] = {}
111
+ for i in issues:
112
+ t = i.get("type", "unknown")
113
+ counts[t] = counts.get(t, 0) + 1
114
+
115
+ for t, c in sorted(counts.items(), key=lambda x: x[1], reverse=True):
116
+ print(f"- {t}: {c}")
117
+
118
+ # 4. Sample Issues
119
+ print("\n\033[1m🔍 Sample Issues\033[0m")
120
+ for i in issues[:5]:
121
+ severity = i.get("severity", "info").upper()
122
+ sev_color = "\033[91m" if severity == "ERROR" else "\033[93m"
123
+ print(
124
+ f"{sev_color}[{severity}]\033[0m {i['file']}:{i.get('line', '?')} - {i['message']}"
125
+ )
126
+
127
+ if len(issues) > 5:
128
+ print(f"... and {len(issues) - 5} more issues.")
129
+
130
+ print("\n" + "=" * 45)
131
+ return True
132
+
133
+
134
+ def _report_by_modules(data: Dict[str, Any]) -> bool:
135
+ """Prints summary grouped by modules."""
136
+ print("\n\033[1m📁 Summary by Modules (Top 10 by Issues)\033[0m")
137
+ print("=" * 60)
138
+
139
+ modules = data.get("modules", [])
140
+ if not modules:
141
+ print("No module data found.")
142
+ return True
143
+
144
+ # Calculate issues per module
145
+ mod_stats = []
146
+ for m in modules:
147
+ issues_count = len(m.get("ast_issues", []))
148
+ mod_stats.append(
149
+ {
150
+ "path": m.get("path"),
151
+ "issues": issues_count,
152
+ "complexity": m.get("complexity", 1),
153
+ "lines": m.get("lines", 0),
154
+ }
155
+ )
156
+
157
+ # Sort by issues (descending)
158
+ mod_stats.sort(key=lambda x: x["issues"], reverse=True)
159
+
160
+ print(f"{'Module Path':<40} | {'Issues':<6} | {'CC':<3} | {'Lines':<5}")
161
+ print("-" * 60)
162
+ for m in mod_stats[:10]:
163
+ print(f"{m['path']:<40} | {m['issues']:<6} | {m['complexity']:<3} | {m['lines']:<5}")
164
+
165
+ print("\n" + "=" * 60)
166
+ return True
167
+
168
+
169
+ def _report_by_functions(data: Dict[str, Any]) -> bool:
170
+ """Prints summary grouped by functions (Top 10 by Complexity)."""
171
+ print("\n\033[1m⚡ Summary by Functions (Top 10 by Complexity)\033[0m")
172
+ print("=" * 70)
173
+
174
+ all_funcs = []
175
+ for m in data.get("modules", []):
176
+ mod_path = m.get("path")
177
+ for f in m.get("functions", []):
178
+ f["module"] = mod_path
179
+ all_funcs.append(f)
180
+
181
+ if not all_funcs:
182
+ print("No function data found.")
183
+ return True
184
+
185
+ # Sort by complexity
186
+ all_funcs.sort(key=lambda x: x.get("complexity", 1), reverse=True)
187
+
188
+ print(f"{'Function Name':<30} | {'Complexity':<10} | {'Module'}")
189
+ print("-" * 70)
190
+ for f in all_funcs[:10]:
191
+ cc = f.get("complexity", 1)
192
+ color = "\033[91m" if cc > 15 else ("\033[93m" if cc > 8 else "")
193
+ reset = "\033[0m" if color else ""
194
+ print(f"{f.get('name'):<30} | {color}{cc:<10}{reset} | {f.get('module')}")
195
+
196
+ print("\n" + "=" * 70)
197
+ return True
198
+
199
+
200
+ def _report_by_classes(data: Dict[str, Any]) -> bool:
201
+ """Prints summary grouped by classes."""
202
+ print("\n\033[1m🏛️ Summary by Classes\033[0m")
203
+ print("=" * 60)
204
+
205
+ all_classes = []
206
+ for m in data.get("modules", []):
207
+ mod_path = m.get("path")
208
+ for c in m.get("classes", []):
209
+ all_classes.append({"name": c, "module": mod_path})
210
+
211
+ if not all_classes:
212
+ print("No class data found.")
213
+ return True
214
+
215
+ print(f"{'Class Name':<30} | {'Module'}")
216
+ print("-" * 60)
217
+ for c in all_classes:
218
+ print(f"{c['name']:<30} | {c['module']}")
219
+
220
+ print(f"\nTotal: {len(all_classes)} classes found.")
221
+ print("=" * 60)
222
+ return True
@@ -0,0 +1,10 @@
1
+ """Rules package for the QGIS Plugin Analyzer."""
2
+
3
+ from .modernization_rules import get_modernization_rules
4
+ from .qgis_rules import I18N_METHODS, get_qgis_audit_rules
5
+
6
+ __all__ = [
7
+ "get_qgis_audit_rules",
8
+ "I18N_METHODS",
9
+ "get_modernization_rules",
10
+ ]
@@ -0,0 +1,33 @@
1
+ """Modernization and research-based quality rules.
2
+
3
+ This module defines rules for detecting modernization opportunities and
4
+ adherence to industry-standard Python practices (Google, Microsoft, PSF).
5
+ """
6
+
7
+ from typing import Any, Dict, List
8
+
9
+
10
+ def get_modernization_rules() -> List[Dict[str, Any]]:
11
+ """Returns the modernization and research-based rule catalog.
12
+
13
+ Returns:
14
+ A list of dictionaries defining quality and modernization rules,
15
+ including messages and severity levels.
16
+ """
17
+ return [
18
+ {
19
+ "id": "MISSING_DOCSTRING",
20
+ "message": "Public module, class, or function missing docstring (PEP 257).",
21
+ "severity": "medium",
22
+ },
23
+ {
24
+ "id": "MISSING_TYPE_HINTS",
25
+ "message": "Function signature missing type annotations (PEP 484).",
26
+ "severity": "low",
27
+ },
28
+ {
29
+ "id": "NON_PYTHONIC_LOOP",
30
+ "message": "Manual loop counter detected. Use enumerate() for clean, Pythonic code.",
31
+ "severity": "medium",
32
+ },
33
+ ]
@@ -0,0 +1,74 @@
1
+ """QGIS-specific audit rules and patterns.
2
+
3
+ This module defines rules for detecting common pitfalls and technical debt
4
+ in PyQGIS plugins.
5
+ """
6
+
7
+ import re
8
+ from typing import Any, Dict, List
9
+
10
+
11
+ def get_qgis_audit_rules() -> List[Dict[str, Any]]:
12
+ """Returns the QGIS audit rule catalog.
13
+
14
+ Returns:
15
+ A list of dictionaries defining QGIS-specific rules, including
16
+ patterns, messages, and severity levels.
17
+ """
18
+ return [
19
+ {
20
+ "id": "UNPRECISE_LAYER",
21
+ "pattern": re.compile(r"mapLayersByName\("),
22
+ "message": "mapLayersByName() can be imprecise. Consider mapLayers() or unique IDs.",
23
+ "severity": "medium",
24
+ },
25
+ {
26
+ "id": "UNSAFE_THREAD",
27
+ "pattern": re.compile(r"\bthreading\.Thread\("),
28
+ "message": "threading.Thread usage detected. Prefer QgsTask or QThread.",
29
+ "severity": "high",
30
+ },
31
+ {
32
+ "id": "MANUAL_RESOURCE_PATH",
33
+ "pattern": re.compile(r"QIcon\(\s*['\"](?!\s*:\/)[^'\"]*?(?:icons|images|ui)/"),
34
+ "message": "Manual resource path detected. Use :/plugins/...",
35
+ "severity": "medium",
36
+ },
37
+ {
38
+ "id": "PRINT_STATEMENT",
39
+ "pattern": re.compile(r"^[^#]*\bprint\("),
40
+ "message": "print() usage detected. Use QgsMessageLog.",
41
+ "severity": "low",
42
+ },
43
+ {
44
+ "id": "OBSOLETE_VARIANT",
45
+ "pattern": re.compile(
46
+ r"QVariant\.(?:String|Int|Double|LongLong|Bool|Date|Time|DateTime)"
47
+ ),
48
+ "message": "Obsolete QVariant type constants detected. Use QMetaType or native types.",
49
+ "severity": "medium",
50
+ },
51
+ {
52
+ "id": "UNSAFE_SUBPROCESS",
53
+ "pattern": re.compile(r"\bsubprocess\.(?:run|call|Popen|check_call|check_output)\("),
54
+ "message": "Potential unsafe subprocess usage. Avoid shell=True and ensure arguments are properly quoted.",
55
+ "severity": "high",
56
+ },
57
+ {
58
+ "id": "BLOCKING_NETWORK_CALL",
59
+ "pattern": re.compile(r"\b(?:requests\.(?:get|post|put|delete|patch)|urllib\.request\.urlopen)\("),
60
+ "message": "Synchronous network call detected. UI blocking risk. Use QgsTask or QNetworkAccessManager.",
61
+ "severity": "high",
62
+ },
63
+ ]
64
+
65
+
66
+ # Methods that require translation in QGIS
67
+ I18N_METHODS = {
68
+ "setText",
69
+ "setWindowTitle",
70
+ "setTitle",
71
+ "setToolTip",
72
+ "setPlaceholderText",
73
+ "setTabText",
74
+ }