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.

Files changed (82) hide show
  1. gwc_pybundle-2.1.2.dist-info/METADATA +903 -0
  2. gwc_pybundle-2.1.2.dist-info/RECORD +82 -0
  3. gwc_pybundle-2.1.2.dist-info/WHEEL +5 -0
  4. gwc_pybundle-2.1.2.dist-info/entry_points.txt +2 -0
  5. gwc_pybundle-2.1.2.dist-info/licenses/LICENSE.md +25 -0
  6. gwc_pybundle-2.1.2.dist-info/top_level.txt +1 -0
  7. pybundle/__init__.py +0 -0
  8. pybundle/__main__.py +4 -0
  9. pybundle/cli.py +546 -0
  10. pybundle/context.py +404 -0
  11. pybundle/doctor.py +148 -0
  12. pybundle/filters.py +228 -0
  13. pybundle/manifest.py +77 -0
  14. pybundle/packaging.py +45 -0
  15. pybundle/policy.py +132 -0
  16. pybundle/profiles.py +454 -0
  17. pybundle/roadmap_model.py +42 -0
  18. pybundle/roadmap_scan.py +328 -0
  19. pybundle/root_detect.py +14 -0
  20. pybundle/runner.py +180 -0
  21. pybundle/steps/__init__.py +26 -0
  22. pybundle/steps/ai_context.py +791 -0
  23. pybundle/steps/api_docs.py +219 -0
  24. pybundle/steps/asyncio_analysis.py +358 -0
  25. pybundle/steps/bandit.py +72 -0
  26. pybundle/steps/base.py +20 -0
  27. pybundle/steps/blocking_call_detection.py +291 -0
  28. pybundle/steps/call_graph.py +219 -0
  29. pybundle/steps/compileall.py +76 -0
  30. pybundle/steps/config_docs.py +319 -0
  31. pybundle/steps/config_validation.py +302 -0
  32. pybundle/steps/container_image.py +294 -0
  33. pybundle/steps/context_expand.py +272 -0
  34. pybundle/steps/copy_pack.py +293 -0
  35. pybundle/steps/coverage.py +101 -0
  36. pybundle/steps/cprofile_step.py +166 -0
  37. pybundle/steps/dependency_sizes.py +136 -0
  38. pybundle/steps/django_checks.py +214 -0
  39. pybundle/steps/dockerfile_lint.py +282 -0
  40. pybundle/steps/dockerignore.py +311 -0
  41. pybundle/steps/duplication.py +103 -0
  42. pybundle/steps/env_completeness.py +269 -0
  43. pybundle/steps/env_var_usage.py +253 -0
  44. pybundle/steps/error_refs.py +204 -0
  45. pybundle/steps/event_loop_patterns.py +280 -0
  46. pybundle/steps/exception_patterns.py +190 -0
  47. pybundle/steps/fastapi_integration.py +250 -0
  48. pybundle/steps/flask_debugging.py +312 -0
  49. pybundle/steps/git_analytics.py +315 -0
  50. pybundle/steps/handoff_md.py +176 -0
  51. pybundle/steps/import_time.py +175 -0
  52. pybundle/steps/interrogate.py +106 -0
  53. pybundle/steps/license_scan.py +96 -0
  54. pybundle/steps/line_profiler.py +117 -0
  55. pybundle/steps/link_validation.py +287 -0
  56. pybundle/steps/logging_analysis.py +233 -0
  57. pybundle/steps/memory_profile.py +176 -0
  58. pybundle/steps/migration_history.py +336 -0
  59. pybundle/steps/mutation_testing.py +141 -0
  60. pybundle/steps/mypy.py +103 -0
  61. pybundle/steps/orm_optimization.py +316 -0
  62. pybundle/steps/pip_audit.py +45 -0
  63. pybundle/steps/pipdeptree.py +62 -0
  64. pybundle/steps/pylance.py +562 -0
  65. pybundle/steps/pytest.py +66 -0
  66. pybundle/steps/query_pattern_analysis.py +334 -0
  67. pybundle/steps/radon.py +161 -0
  68. pybundle/steps/repro_md.py +161 -0
  69. pybundle/steps/rg_scans.py +78 -0
  70. pybundle/steps/roadmap.py +153 -0
  71. pybundle/steps/ruff.py +117 -0
  72. pybundle/steps/secrets_detection.py +235 -0
  73. pybundle/steps/security_headers.py +309 -0
  74. pybundle/steps/shell.py +74 -0
  75. pybundle/steps/slow_tests.py +178 -0
  76. pybundle/steps/sqlalchemy_validation.py +269 -0
  77. pybundle/steps/test_flakiness.py +184 -0
  78. pybundle/steps/tree.py +116 -0
  79. pybundle/steps/type_coverage.py +277 -0
  80. pybundle/steps/unused_deps.py +211 -0
  81. pybundle/steps/vulture.py +167 -0
  82. 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"