gwc-pybundle 2.1.2__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.
Potentially problematic release.
This version of gwc-pybundle might be problematic. Click here for more details.
- gwc_pybundle-2.1.2.dist-info/METADATA +903 -0
- gwc_pybundle-2.1.2.dist-info/RECORD +82 -0
- gwc_pybundle-2.1.2.dist-info/WHEEL +5 -0
- gwc_pybundle-2.1.2.dist-info/entry_points.txt +2 -0
- gwc_pybundle-2.1.2.dist-info/licenses/LICENSE.md +25 -0
- gwc_pybundle-2.1.2.dist-info/top_level.txt +1 -0
- pybundle/__init__.py +0 -0
- pybundle/__main__.py +4 -0
- pybundle/cli.py +546 -0
- pybundle/context.py +404 -0
- pybundle/doctor.py +148 -0
- pybundle/filters.py +228 -0
- pybundle/manifest.py +77 -0
- pybundle/packaging.py +45 -0
- pybundle/policy.py +132 -0
- pybundle/profiles.py +454 -0
- pybundle/roadmap_model.py +42 -0
- pybundle/roadmap_scan.py +328 -0
- pybundle/root_detect.py +14 -0
- pybundle/runner.py +180 -0
- pybundle/steps/__init__.py +26 -0
- pybundle/steps/ai_context.py +791 -0
- pybundle/steps/api_docs.py +219 -0
- pybundle/steps/asyncio_analysis.py +358 -0
- pybundle/steps/bandit.py +72 -0
- pybundle/steps/base.py +20 -0
- pybundle/steps/blocking_call_detection.py +291 -0
- pybundle/steps/call_graph.py +219 -0
- pybundle/steps/compileall.py +76 -0
- pybundle/steps/config_docs.py +319 -0
- pybundle/steps/config_validation.py +302 -0
- pybundle/steps/container_image.py +294 -0
- pybundle/steps/context_expand.py +272 -0
- pybundle/steps/copy_pack.py +293 -0
- pybundle/steps/coverage.py +101 -0
- pybundle/steps/cprofile_step.py +166 -0
- pybundle/steps/dependency_sizes.py +136 -0
- pybundle/steps/django_checks.py +214 -0
- pybundle/steps/dockerfile_lint.py +282 -0
- pybundle/steps/dockerignore.py +311 -0
- pybundle/steps/duplication.py +103 -0
- pybundle/steps/env_completeness.py +269 -0
- pybundle/steps/env_var_usage.py +253 -0
- pybundle/steps/error_refs.py +204 -0
- pybundle/steps/event_loop_patterns.py +280 -0
- pybundle/steps/exception_patterns.py +190 -0
- pybundle/steps/fastapi_integration.py +250 -0
- pybundle/steps/flask_debugging.py +312 -0
- pybundle/steps/git_analytics.py +315 -0
- pybundle/steps/handoff_md.py +176 -0
- pybundle/steps/import_time.py +175 -0
- pybundle/steps/interrogate.py +106 -0
- pybundle/steps/license_scan.py +96 -0
- pybundle/steps/line_profiler.py +117 -0
- pybundle/steps/link_validation.py +287 -0
- pybundle/steps/logging_analysis.py +233 -0
- pybundle/steps/memory_profile.py +176 -0
- pybundle/steps/migration_history.py +336 -0
- pybundle/steps/mutation_testing.py +141 -0
- pybundle/steps/mypy.py +103 -0
- pybundle/steps/orm_optimization.py +316 -0
- pybundle/steps/pip_audit.py +45 -0
- pybundle/steps/pipdeptree.py +62 -0
- pybundle/steps/pylance.py +562 -0
- pybundle/steps/pytest.py +66 -0
- pybundle/steps/query_pattern_analysis.py +334 -0
- pybundle/steps/radon.py +161 -0
- pybundle/steps/repro_md.py +161 -0
- pybundle/steps/rg_scans.py +78 -0
- pybundle/steps/roadmap.py +153 -0
- pybundle/steps/ruff.py +117 -0
- pybundle/steps/secrets_detection.py +235 -0
- pybundle/steps/security_headers.py +309 -0
- pybundle/steps/shell.py +74 -0
- pybundle/steps/slow_tests.py +178 -0
- pybundle/steps/sqlalchemy_validation.py +269 -0
- pybundle/steps/test_flakiness.py +184 -0
- pybundle/steps/tree.py +116 -0
- pybundle/steps/type_coverage.py +277 -0
- pybundle/steps/unused_deps.py +211 -0
- pybundle/steps/vulture.py +167 -0
- pybundle/tools.py +63 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Step: Event Loop Patterns Analysis
|
|
3
|
+
Analyze event loop creation and usage patterns.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import ast
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Set, Tuple, Optional
|
|
10
|
+
|
|
11
|
+
from .base import Step, StepResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventLoopPatternsStep(Step):
|
|
15
|
+
"""Analyze event loop patterns and best practices."""
|
|
16
|
+
|
|
17
|
+
name = "event loop patterns"
|
|
18
|
+
|
|
19
|
+
def run(self, ctx: "BundleContext") -> StepResult: # type: ignore[name-defined]
|
|
20
|
+
"""Analyze event loop patterns in codebase."""
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
start = time.time()
|
|
24
|
+
|
|
25
|
+
root = ctx.root
|
|
26
|
+
|
|
27
|
+
# Find event loop patterns
|
|
28
|
+
patterns = self._find_event_loop_patterns(root)
|
|
29
|
+
|
|
30
|
+
# Generate report
|
|
31
|
+
lines = [
|
|
32
|
+
"=" * 80,
|
|
33
|
+
"EVENT LOOP PATTERNS ANALYSIS REPORT",
|
|
34
|
+
"=" * 80,
|
|
35
|
+
"",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Summary
|
|
39
|
+
lines.extend(
|
|
40
|
+
[
|
|
41
|
+
"SUMMARY",
|
|
42
|
+
"=" * 80,
|
|
43
|
+
"",
|
|
44
|
+
f"Event loop creations found: {len(patterns['asyncio_run'])}",
|
|
45
|
+
f"get_event_loop() calls: {len(patterns['get_event_loop'])}",
|
|
46
|
+
f"new_event_loop() calls: {len(patterns['new_event_loop'])}",
|
|
47
|
+
f"Event loop close() calls: {len(patterns['close'])}",
|
|
48
|
+
"",
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if not patterns["has_async"]:
|
|
53
|
+
lines.extend(
|
|
54
|
+
[
|
|
55
|
+
"⊘ No event loop patterns detected",
|
|
56
|
+
"",
|
|
57
|
+
"This project does not appear to use explicit event loop management.",
|
|
58
|
+
"If this is incorrect, ensure asyncio code is in analyzed files.",
|
|
59
|
+
"",
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
# Event loop creation patterns
|
|
64
|
+
lines.extend(
|
|
65
|
+
[
|
|
66
|
+
"EVENT LOOP CREATION PATTERNS",
|
|
67
|
+
"=" * 80,
|
|
68
|
+
"",
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if patterns["asyncio_run"]:
|
|
73
|
+
lines.append("✓ ASYNCIO.RUN (Recommended - Python 3.7+):")
|
|
74
|
+
lines.append(" Creates and closes event loop automatically")
|
|
75
|
+
for item in patterns["asyncio_run"][:10]:
|
|
76
|
+
lines.append(f" {item}")
|
|
77
|
+
if len(patterns["asyncio_run"]) > 10:
|
|
78
|
+
lines.append(f" ... and {len(patterns['asyncio_run']) - 10} more")
|
|
79
|
+
lines.append("")
|
|
80
|
+
|
|
81
|
+
if patterns["get_event_loop"]:
|
|
82
|
+
lines.append("⚠ GET_EVENT_LOOP (Legacy Pattern):")
|
|
83
|
+
lines.append(" Deprecated in Python 3.10+, can raise DeprecationWarning")
|
|
84
|
+
for item in patterns["get_event_loop"][:10]:
|
|
85
|
+
lines.append(f" {item}")
|
|
86
|
+
if len(patterns["get_event_loop"]) > 10:
|
|
87
|
+
lines.append(f" ... and {len(patterns['get_event_loop']) - 10} more")
|
|
88
|
+
lines.append("")
|
|
89
|
+
|
|
90
|
+
if patterns["new_event_loop"]:
|
|
91
|
+
lines.append("⚠ NEW_EVENT_LOOP (Manual Management):")
|
|
92
|
+
lines.append(" Less convenient than asyncio.run(), requires explicit close()")
|
|
93
|
+
for item in patterns["new_event_loop"][:10]:
|
|
94
|
+
lines.append(f" {item}")
|
|
95
|
+
if len(patterns["new_event_loop"]) > 10:
|
|
96
|
+
lines.append(f" ... and {len(patterns['new_event_loop']) - 10} more")
|
|
97
|
+
lines.append("")
|
|
98
|
+
|
|
99
|
+
# Resource management
|
|
100
|
+
lines.extend(
|
|
101
|
+
[
|
|
102
|
+
"RESOURCE MANAGEMENT",
|
|
103
|
+
"=" * 80,
|
|
104
|
+
"",
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if patterns["close"]:
|
|
109
|
+
lines.append(f"✓ Event loop close() calls: {len(patterns['close'])}")
|
|
110
|
+
else:
|
|
111
|
+
if patterns["get_event_loop"] or patterns["new_event_loop"]:
|
|
112
|
+
lines.append("⚠ No explicit loop.close() calls found")
|
|
113
|
+
lines.append(" Event loops created with get_event_loop() or new_event_loop()")
|
|
114
|
+
lines.append(" should be explicitly closed to avoid resource leaks")
|
|
115
|
+
else:
|
|
116
|
+
lines.append("ℹ No explicit close() calls needed (using asyncio.run)")
|
|
117
|
+
|
|
118
|
+
lines.append("")
|
|
119
|
+
|
|
120
|
+
# Best practices
|
|
121
|
+
lines.extend(
|
|
122
|
+
[
|
|
123
|
+
"BEST PRACTICES ANALYSIS",
|
|
124
|
+
"=" * 80,
|
|
125
|
+
"",
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Check Python version requirement
|
|
130
|
+
lines.append("Python Version Compatibility:")
|
|
131
|
+
lines.append("")
|
|
132
|
+
|
|
133
|
+
if patterns["asyncio_run"]:
|
|
134
|
+
lines.append(" ✓ asyncio.run() requires Python 3.7+")
|
|
135
|
+
lines.append(" Check pyproject.toml requires-python >= 3.7")
|
|
136
|
+
else:
|
|
137
|
+
lines.append(" ℹ No asyncio.run() usage found")
|
|
138
|
+
|
|
139
|
+
lines.append("")
|
|
140
|
+
|
|
141
|
+
# Running mode
|
|
142
|
+
if patterns["asyncio_run"] and not patterns["get_event_loop"]:
|
|
143
|
+
lines.append(" ✓ Consistent event loop pattern (asyncio.run)")
|
|
144
|
+
elif patterns["get_event_loop"] and not patterns["asyncio_run"]:
|
|
145
|
+
lines.append(" ⚠ Using legacy get_event_loop() pattern")
|
|
146
|
+
lines.append(" Consider migrating to asyncio.run() for clarity")
|
|
147
|
+
elif patterns["asyncio_run"] and patterns["get_event_loop"]:
|
|
148
|
+
lines.append(" ⚠ Mixed event loop patterns (both asyncio.run and get_event_loop)")
|
|
149
|
+
lines.append(" Consider standardizing on asyncio.run()")
|
|
150
|
+
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
# Context managers
|
|
154
|
+
if patterns["async_with_count"] > 0:
|
|
155
|
+
lines.append(f" ✓ Using async with statements: {patterns['async_with_count']} instances")
|
|
156
|
+
else:
|
|
157
|
+
if patterns["has_async"]:
|
|
158
|
+
lines.append(" ℹ No async with statements found")
|
|
159
|
+
|
|
160
|
+
lines.append("")
|
|
161
|
+
|
|
162
|
+
# Recommendations
|
|
163
|
+
lines.extend(
|
|
164
|
+
[
|
|
165
|
+
"=" * 80,
|
|
166
|
+
"RECOMMENDATIONS",
|
|
167
|
+
"=" * 80,
|
|
168
|
+
"",
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if patterns["has_async"]:
|
|
173
|
+
if patterns["get_event_loop"] and not patterns["asyncio_run"]:
|
|
174
|
+
lines.append(" 1. Migrate from get_event_loop() to asyncio.run()")
|
|
175
|
+
lines.append("")
|
|
176
|
+
lines.append(" Before (Python 3.7-3.9):")
|
|
177
|
+
lines.append(" loop = asyncio.get_event_loop()")
|
|
178
|
+
lines.append(" try:")
|
|
179
|
+
lines.append(" loop.run_until_complete(main())")
|
|
180
|
+
lines.append(" finally:")
|
|
181
|
+
lines.append(" loop.close()")
|
|
182
|
+
lines.append("")
|
|
183
|
+
lines.append(" After (Python 3.7+):")
|
|
184
|
+
lines.append(" asyncio.run(main())")
|
|
185
|
+
lines.append("")
|
|
186
|
+
|
|
187
|
+
lines.append(" - Use asyncio.run() as entry point (automatically manages loop)")
|
|
188
|
+
lines.append(" - Use async with for resource management in async code")
|
|
189
|
+
lines.append(" - Avoid get_event_loop() except in special cases")
|
|
190
|
+
lines.append(" - Consider asyncio.Runner for multiple runs (Python 3.11+)")
|
|
191
|
+
lines.append(" - Document asyncio requirements in README")
|
|
192
|
+
|
|
193
|
+
else:
|
|
194
|
+
lines.append(" - No event loop patterns detected in code")
|
|
195
|
+
lines.append(" - If planning to use asyncio, use asyncio.run() as main entry point")
|
|
196
|
+
lines.append(" - Review async best practices: https://docs.python.org/3/library/asyncio-dev.html")
|
|
197
|
+
|
|
198
|
+
lines.append("")
|
|
199
|
+
|
|
200
|
+
# Write report
|
|
201
|
+
output = "\n".join(lines)
|
|
202
|
+
dest = ctx.workdir / "logs" / "132_event_loop_patterns.txt"
|
|
203
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
dest.write_text(output, encoding="utf-8")
|
|
205
|
+
|
|
206
|
+
elapsed = int(time.time() - start)
|
|
207
|
+
return StepResult(self.name, "OK", elapsed, "")
|
|
208
|
+
|
|
209
|
+
def _find_event_loop_patterns(self, root: Path) -> Dict:
|
|
210
|
+
"""Find event loop creation and usage patterns."""
|
|
211
|
+
asyncio_run = []
|
|
212
|
+
get_event_loop = []
|
|
213
|
+
new_event_loop = []
|
|
214
|
+
close = []
|
|
215
|
+
async_with_count = 0
|
|
216
|
+
has_async = False
|
|
217
|
+
|
|
218
|
+
python_files = list(root.rglob("*.py"))
|
|
219
|
+
|
|
220
|
+
for py_file in python_files:
|
|
221
|
+
if any(
|
|
222
|
+
part in py_file.parts
|
|
223
|
+
for part in ["venv", ".venv", "env", "__pycache__", "site-packages"]
|
|
224
|
+
):
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
source = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
229
|
+
rel_path = str(py_file.relative_to(root))
|
|
230
|
+
|
|
231
|
+
# Check for async keyword
|
|
232
|
+
if "async " in source:
|
|
233
|
+
has_async = True
|
|
234
|
+
|
|
235
|
+
tree = ast.parse(source)
|
|
236
|
+
|
|
237
|
+
# Count async context managers
|
|
238
|
+
for node in ast.walk(tree):
|
|
239
|
+
if isinstance(node, ast.AsyncWith):
|
|
240
|
+
async_with_count += 1
|
|
241
|
+
|
|
242
|
+
# Find event loop patterns via source regex (for robustness)
|
|
243
|
+
for line_num, line in enumerate(source.split("\n"), 1):
|
|
244
|
+
# asyncio.run() pattern
|
|
245
|
+
if re.search(r"asyncio\.run\s*\(", line):
|
|
246
|
+
asyncio_run.append(f"{rel_path}:{line_num}")
|
|
247
|
+
|
|
248
|
+
# get_event_loop() pattern
|
|
249
|
+
if re.search(
|
|
250
|
+
r"asyncio\.get_event_loop\s*\(|get_event_loop\s*\(", line
|
|
251
|
+
):
|
|
252
|
+
get_event_loop.append(f"{rel_path}:{line_num}")
|
|
253
|
+
|
|
254
|
+
# new_event_loop() pattern
|
|
255
|
+
if re.search(
|
|
256
|
+
r"asyncio\.new_event_loop\s*\(|new_event_loop\s*\(", line
|
|
257
|
+
):
|
|
258
|
+
new_event_loop.append(f"{rel_path}:{line_num}")
|
|
259
|
+
|
|
260
|
+
# close() pattern
|
|
261
|
+
if re.search(r"(loop|event_loop)\.close\s*\(", line):
|
|
262
|
+
close.append(f"{rel_path}:{line_num}")
|
|
263
|
+
|
|
264
|
+
except (OSError, UnicodeDecodeError, SyntaxError):
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
# Deduplicate
|
|
268
|
+
asyncio_run = list(set(asyncio_run))
|
|
269
|
+
get_event_loop = list(set(get_event_loop))
|
|
270
|
+
new_event_loop = list(set(new_event_loop))
|
|
271
|
+
close = list(set(close))
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
"asyncio_run": sorted(asyncio_run),
|
|
275
|
+
"get_event_loop": sorted(get_event_loop),
|
|
276
|
+
"new_event_loop": sorted(new_event_loop),
|
|
277
|
+
"close": sorted(close),
|
|
278
|
+
"async_with_count": async_with_count,
|
|
279
|
+
"has_async": has_async,
|
|
280
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Step: Exception Pattern Tracking
|
|
3
|
+
Track all raise statements and categorize exception types.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Set
|
|
10
|
+
|
|
11
|
+
from .base import Step, StepResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExceptionPatternsStep(Step):
|
|
15
|
+
"""Analyze exception patterns and raise statements in Python code."""
|
|
16
|
+
|
|
17
|
+
name = "exception patterns"
|
|
18
|
+
|
|
19
|
+
def run(self, ctx: "BundleContext") -> StepResult: # type: ignore[name-defined]
|
|
20
|
+
"""Find all raise statements and categorize exception types."""
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
start = time.time()
|
|
24
|
+
|
|
25
|
+
root = ctx.root
|
|
26
|
+
python_files = sorted(root.rglob("*.py"))
|
|
27
|
+
if not python_files:
|
|
28
|
+
return StepResult(self.name, "SKIP", int(time.time() - start), "No Python files found")
|
|
29
|
+
|
|
30
|
+
# Track exception patterns
|
|
31
|
+
exception_types: Dict[str, List[str]] = {} # exception_type -> [file:line, ...]
|
|
32
|
+
custom_exceptions: Set[str] = set()
|
|
33
|
+
bare_raises = [] # re-raise without argument
|
|
34
|
+
exception_chaining = [] # raise ... from ...
|
|
35
|
+
analyzed_files = 0
|
|
36
|
+
|
|
37
|
+
for py_file in python_files:
|
|
38
|
+
# Skip non-user code
|
|
39
|
+
if any(
|
|
40
|
+
part in py_file.parts
|
|
41
|
+
for part in [
|
|
42
|
+
"venv",
|
|
43
|
+
".venv",
|
|
44
|
+
"env",
|
|
45
|
+
"site-packages",
|
|
46
|
+
"__pycache__",
|
|
47
|
+
".git",
|
|
48
|
+
"node_modules",
|
|
49
|
+
]
|
|
50
|
+
):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
analyzed_files += 1
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
source = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
57
|
+
tree = ast.parse(source, str(py_file))
|
|
58
|
+
|
|
59
|
+
for node in ast.walk(tree):
|
|
60
|
+
if isinstance(node, ast.Raise):
|
|
61
|
+
rel_path = py_file.relative_to(root)
|
|
62
|
+
location = f"{rel_path}:{node.lineno}"
|
|
63
|
+
|
|
64
|
+
if node.exc is None:
|
|
65
|
+
# Bare raise (re-raise)
|
|
66
|
+
bare_raises.append(location)
|
|
67
|
+
else:
|
|
68
|
+
# Extract exception type
|
|
69
|
+
exc_type = self._extract_exception_type(node.exc)
|
|
70
|
+
if exc_type:
|
|
71
|
+
if exc_type not in exception_types:
|
|
72
|
+
exception_types[exc_type] = []
|
|
73
|
+
exception_types[exc_type].append(location)
|
|
74
|
+
|
|
75
|
+
# Check if it's a custom exception (not in builtins)
|
|
76
|
+
if exc_type not in dir(__builtins__) and not exc_type.startswith(
|
|
77
|
+
("OSError", "IOError", "ValueError", "TypeError", "RuntimeError")
|
|
78
|
+
):
|
|
79
|
+
custom_exceptions.add(exc_type)
|
|
80
|
+
|
|
81
|
+
# Check for exception chaining (raise ... from ...)
|
|
82
|
+
if node.cause is not None:
|
|
83
|
+
exception_chaining.append(location)
|
|
84
|
+
|
|
85
|
+
except (SyntaxError, UnicodeDecodeError):
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# Generate report
|
|
89
|
+
lines = [
|
|
90
|
+
"=" * 80,
|
|
91
|
+
"EXCEPTION PATTERN ANALYSIS",
|
|
92
|
+
"=" * 80,
|
|
93
|
+
"",
|
|
94
|
+
f"Total Python files analyzed: {analyzed_files}",
|
|
95
|
+
f"Total exception types found: {len(exception_types)}",
|
|
96
|
+
f"Custom exceptions: {len(custom_exceptions)}",
|
|
97
|
+
f"Bare raises (re-raise): {len(bare_raises)}",
|
|
98
|
+
f"Exception chaining (raise...from): {len(exception_chaining)}",
|
|
99
|
+
"",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
# Exception type breakdown
|
|
103
|
+
if exception_types:
|
|
104
|
+
lines.extend(
|
|
105
|
+
[
|
|
106
|
+
"=" * 80,
|
|
107
|
+
"EXCEPTION TYPES (sorted by frequency)",
|
|
108
|
+
"=" * 80,
|
|
109
|
+
"",
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
sorted_exceptions = sorted(exception_types.items(), key=lambda x: len(x[1]), reverse=True)
|
|
114
|
+
for exc_type, locations in sorted_exceptions:
|
|
115
|
+
lines.append(f"{exc_type}: {len(locations)} occurrence(s)")
|
|
116
|
+
for loc in locations[:5]: # Show first 5 locations
|
|
117
|
+
lines.append(f" - {loc}")
|
|
118
|
+
if len(locations) > 5:
|
|
119
|
+
lines.append(f" ... and {len(locations) - 5} more")
|
|
120
|
+
lines.append("")
|
|
121
|
+
|
|
122
|
+
# Custom exceptions
|
|
123
|
+
if custom_exceptions:
|
|
124
|
+
lines.extend(
|
|
125
|
+
[
|
|
126
|
+
"=" * 80,
|
|
127
|
+
"CUSTOM EXCEPTIONS",
|
|
128
|
+
"=" * 80,
|
|
129
|
+
"",
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
for exc in sorted(custom_exceptions):
|
|
133
|
+
lines.append(f" - {exc}")
|
|
134
|
+
lines.append("")
|
|
135
|
+
|
|
136
|
+
# Bare raises
|
|
137
|
+
if bare_raises:
|
|
138
|
+
lines.extend(
|
|
139
|
+
[
|
|
140
|
+
"=" * 80,
|
|
141
|
+
"BARE RAISES (re-raise without argument)",
|
|
142
|
+
"=" * 80,
|
|
143
|
+
"",
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
for loc in bare_raises:
|
|
147
|
+
lines.append(f" - {loc}")
|
|
148
|
+
lines.append("")
|
|
149
|
+
|
|
150
|
+
# Exception chaining
|
|
151
|
+
if exception_chaining:
|
|
152
|
+
lines.extend(
|
|
153
|
+
[
|
|
154
|
+
"=" * 80,
|
|
155
|
+
"EXCEPTION CHAINING (raise...from)",
|
|
156
|
+
"=" * 80,
|
|
157
|
+
"",
|
|
158
|
+
]
|
|
159
|
+
)
|
|
160
|
+
for loc in exception_chaining:
|
|
161
|
+
lines.append(f" - {loc}")
|
|
162
|
+
lines.append("")
|
|
163
|
+
|
|
164
|
+
# Write report
|
|
165
|
+
output = "\n".join(lines)
|
|
166
|
+
dest = ctx.workdir / "meta" / "101_exception_patterns.txt"
|
|
167
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
dest.write_text(output, encoding="utf-8")
|
|
169
|
+
|
|
170
|
+
elapsed = int(time.time() - start)
|
|
171
|
+
return StepResult(self.name, "OK", elapsed, "")
|
|
172
|
+
|
|
173
|
+
def _extract_exception_type(self, node: ast.expr) -> str:
|
|
174
|
+
"""Extract exception type name from AST node."""
|
|
175
|
+
if isinstance(node, ast.Name):
|
|
176
|
+
return node.id
|
|
177
|
+
elif isinstance(node, ast.Call):
|
|
178
|
+
# Exception instantiation: ValueError("msg")
|
|
179
|
+
return self._extract_exception_type(node.func)
|
|
180
|
+
elif isinstance(node, ast.Attribute):
|
|
181
|
+
# Module exception: module.ExceptionType
|
|
182
|
+
parts = []
|
|
183
|
+
current = node
|
|
184
|
+
while isinstance(current, ast.Attribute):
|
|
185
|
+
parts.insert(0, current.attr)
|
|
186
|
+
current = current.value
|
|
187
|
+
if isinstance(current, ast.Name):
|
|
188
|
+
parts.insert(0, current.id)
|
|
189
|
+
return ".".join(parts)
|
|
190
|
+
return "Unknown"
|