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,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Step: ORM Optimization Analysis
|
|
3
|
+
Analyze ORM usage and provide optimization suggestions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Set, Tuple, Optional
|
|
9
|
+
|
|
10
|
+
from .base import Step, StepResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ORMOptimizationStep(Step):
|
|
14
|
+
"""Analyze ORM usage patterns and suggest optimizations."""
|
|
15
|
+
|
|
16
|
+
name = "orm optimization"
|
|
17
|
+
|
|
18
|
+
# Django-specific patterns
|
|
19
|
+
DJANGO_PATTERNS = {
|
|
20
|
+
"bulk_create": r"\.bulk_create\(",
|
|
21
|
+
"bulk_update": r"\.bulk_update\(",
|
|
22
|
+
"select_related": r"\.select_related\(",
|
|
23
|
+
"prefetch_related": r"\.prefetch_related\(",
|
|
24
|
+
"only": r"\.only\(",
|
|
25
|
+
"defer": r"\.defer\(",
|
|
26
|
+
"values": r"\.values\(",
|
|
27
|
+
"values_list": r"\.values_list\(",
|
|
28
|
+
"exists": r"\.exists\(",
|
|
29
|
+
"count": r"\.count\(",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# SQLAlchemy patterns
|
|
33
|
+
SQLALCHEMY_PATTERNS = {
|
|
34
|
+
"joinedload": r"joinedload\(",
|
|
35
|
+
"selectinload": r"selectinload\(",
|
|
36
|
+
"contains_eager": r"contains_eager\(",
|
|
37
|
+
"defer": r"defer\(",
|
|
38
|
+
"load_only": r"load_only\(",
|
|
39
|
+
"raiseload": r"raiseload\(",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def run(self, ctx: "BundleContext") -> StepResult: # type: ignore[name-defined]
|
|
43
|
+
"""Analyze ORM optimization patterns."""
|
|
44
|
+
import time
|
|
45
|
+
|
|
46
|
+
start = time.time()
|
|
47
|
+
|
|
48
|
+
root = ctx.root
|
|
49
|
+
|
|
50
|
+
# Analyze ORM usage
|
|
51
|
+
analysis = self._analyze_orm_optimization(root)
|
|
52
|
+
|
|
53
|
+
# Generate report
|
|
54
|
+
lines = [
|
|
55
|
+
"=" * 80,
|
|
56
|
+
"ORM OPTIMIZATION ANALYSIS REPORT",
|
|
57
|
+
"=" * 80,
|
|
58
|
+
"",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# Summary
|
|
62
|
+
lines.extend(
|
|
63
|
+
[
|
|
64
|
+
"SUMMARY",
|
|
65
|
+
"=" * 80,
|
|
66
|
+
"",
|
|
67
|
+
f"Primary ORM: {analysis['primary_orm'] or 'None detected'}",
|
|
68
|
+
"",
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not analysis["primary_orm"]:
|
|
73
|
+
lines.extend(
|
|
74
|
+
[
|
|
75
|
+
"⊘ No ORM detected",
|
|
76
|
+
"",
|
|
77
|
+
"This project does not appear to use a common ORM.",
|
|
78
|
+
"If this is incorrect, ensure ORM imports are visible.",
|
|
79
|
+
"",
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
# Django Analysis
|
|
84
|
+
if analysis["primary_orm"] == "Django":
|
|
85
|
+
lines.extend(
|
|
86
|
+
[
|
|
87
|
+
"DJANGO ORM OPTIMIZATION",
|
|
88
|
+
"=" * 80,
|
|
89
|
+
"",
|
|
90
|
+
]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
django_analysis = analysis["django"]
|
|
94
|
+
|
|
95
|
+
# Optimization techniques used
|
|
96
|
+
lines.append("Optimization Techniques Used:")
|
|
97
|
+
lines.append("")
|
|
98
|
+
|
|
99
|
+
for technique, count in sorted(django_analysis.items()):
|
|
100
|
+
if count > 0:
|
|
101
|
+
lines.append(f" ✓ {technique}: {count} usage(s)")
|
|
102
|
+
|
|
103
|
+
if not any(count > 0 for count in django_analysis.values()):
|
|
104
|
+
lines.append(" ⚠ No optimization techniques detected")
|
|
105
|
+
|
|
106
|
+
lines.append("")
|
|
107
|
+
|
|
108
|
+
# Missing optimizations
|
|
109
|
+
lines.extend(
|
|
110
|
+
[
|
|
111
|
+
"OPTIMIZATION OPPORTUNITIES",
|
|
112
|
+
"-" * 80,
|
|
113
|
+
"",
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if django_analysis.get("select_related", 0) == 0:
|
|
118
|
+
lines.append(
|
|
119
|
+
" • Missing select_related():"
|
|
120
|
+
)
|
|
121
|
+
lines.append(
|
|
122
|
+
" Useful for ForeignKey and OneToOneField relationships"
|
|
123
|
+
)
|
|
124
|
+
lines.append(" Reduces queries by joining related tables")
|
|
125
|
+
lines.append("")
|
|
126
|
+
|
|
127
|
+
if django_analysis.get("prefetch_related", 0) == 0:
|
|
128
|
+
lines.append(
|
|
129
|
+
" • Missing prefetch_related():"
|
|
130
|
+
)
|
|
131
|
+
lines.append(
|
|
132
|
+
" Useful for ManyToManyField and reverse ForeignKey"
|
|
133
|
+
)
|
|
134
|
+
lines.append(
|
|
135
|
+
" Fetches related objects in separate optimized queries"
|
|
136
|
+
)
|
|
137
|
+
lines.append("")
|
|
138
|
+
|
|
139
|
+
if django_analysis.get("only", 0) == 0 and django_analysis.get(
|
|
140
|
+
"defer", 0
|
|
141
|
+
) == 0:
|
|
142
|
+
lines.append(" • Consider using only() or defer():")
|
|
143
|
+
lines.append(" Reduces data transferred from database")
|
|
144
|
+
lines.append(" Useful for large text/blob fields")
|
|
145
|
+
lines.append("")
|
|
146
|
+
|
|
147
|
+
if django_analysis.get("bulk_create", 0) == 0:
|
|
148
|
+
lines.append(" • Missing bulk operations:")
|
|
149
|
+
lines.append(" Use bulk_create() for batch inserts")
|
|
150
|
+
lines.append(" Use bulk_update() for batch updates")
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
if django_analysis.get("exists", 0) == 0:
|
|
154
|
+
lines.append(" • Consider using exists() for existence checks:")
|
|
155
|
+
lines.append(" Faster than count() when just checking presence")
|
|
156
|
+
lines.append("")
|
|
157
|
+
|
|
158
|
+
# SQLAlchemy Analysis
|
|
159
|
+
elif analysis["primary_orm"] == "SQLAlchemy":
|
|
160
|
+
lines.extend(
|
|
161
|
+
[
|
|
162
|
+
"SQLALCHEMY ORM OPTIMIZATION",
|
|
163
|
+
"=" * 80,
|
|
164
|
+
"",
|
|
165
|
+
]
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
sqlalchemy_analysis = analysis["sqlalchemy"]
|
|
169
|
+
|
|
170
|
+
# Optimization techniques used
|
|
171
|
+
lines.append("Eager Loading Techniques Used:")
|
|
172
|
+
lines.append("")
|
|
173
|
+
|
|
174
|
+
for technique, count in sorted(sqlalchemy_analysis.items()):
|
|
175
|
+
if count > 0:
|
|
176
|
+
lines.append(f" ✓ {technique}: {count} usage(s)")
|
|
177
|
+
|
|
178
|
+
if not any(count > 0 for count in sqlalchemy_analysis.values()):
|
|
179
|
+
lines.append(" ⚠ No eager loading techniques detected")
|
|
180
|
+
|
|
181
|
+
lines.append("")
|
|
182
|
+
|
|
183
|
+
# Recommendations
|
|
184
|
+
lines.extend(
|
|
185
|
+
[
|
|
186
|
+
"OPTIMIZATION OPPORTUNITIES",
|
|
187
|
+
"-" * 80,
|
|
188
|
+
"",
|
|
189
|
+
]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if sqlalchemy_analysis.get("joinedload", 0) == 0:
|
|
193
|
+
lines.append(" • Missing joinedload():")
|
|
194
|
+
lines.append(" Performs eager loading with SQL joins")
|
|
195
|
+
lines.append(" Reduces number of queries")
|
|
196
|
+
lines.append("")
|
|
197
|
+
|
|
198
|
+
if sqlalchemy_analysis.get("selectinload", 0) == 0:
|
|
199
|
+
lines.append(" • Missing selectinload():")
|
|
200
|
+
lines.append(" Performs eager loading with IN clause")
|
|
201
|
+
lines.append(" Better for large collections")
|
|
202
|
+
lines.append("")
|
|
203
|
+
|
|
204
|
+
if sqlalchemy_analysis.get("raiseload", 0) == 0:
|
|
205
|
+
lines.append(" • Consider using raiseload():")
|
|
206
|
+
lines.append(" Catches lazy-loaded accesses (helpful for debugging)")
|
|
207
|
+
lines.append("")
|
|
208
|
+
|
|
209
|
+
# Tortoise ORM
|
|
210
|
+
elif analysis["primary_orm"] == "Tortoise ORM":
|
|
211
|
+
lines.extend(
|
|
212
|
+
[
|
|
213
|
+
"TORTOISE ORM OPTIMIZATION",
|
|
214
|
+
"=" * 80,
|
|
215
|
+
"",
|
|
216
|
+
" • Use prefetch_related() for relationships",
|
|
217
|
+
" • Use select_related() for ForeignKey",
|
|
218
|
+
" • Consider indexed fields for frequently filtered columns",
|
|
219
|
+
" • Use bulk_create() for multiple insertions",
|
|
220
|
+
"",
|
|
221
|
+
]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# General recommendations
|
|
225
|
+
lines.extend(
|
|
226
|
+
[
|
|
227
|
+
"=" * 80,
|
|
228
|
+
"GENERAL OPTIMIZATION PRINCIPLES",
|
|
229
|
+
"=" * 80,
|
|
230
|
+
"",
|
|
231
|
+
"1. USE QUERY ANALYSIS TOOLS",
|
|
232
|
+
" - Django Debug Toolbar: Inspect query count and time",
|
|
233
|
+
" - SQLAlchemy echo=True: Log all queries",
|
|
234
|
+
" - Database EXPLAIN: Analyze query execution plans",
|
|
235
|
+
"",
|
|
236
|
+
"2. OPTIMIZE COMMON PATTERNS",
|
|
237
|
+
" - Batch operations (bulk_create, bulk_update)",
|
|
238
|
+
" - Eager loading (select_related, prefetch_related)",
|
|
239
|
+
" - Field selection (only, defer, values, values_list)",
|
|
240
|
+
"",
|
|
241
|
+
"3. DATABASE LEVEL",
|
|
242
|
+
" - Add indexes to filtered/joined columns",
|
|
243
|
+
" - Denormalize if query patterns justify it",
|
|
244
|
+
" - Use database-level aggregations when possible",
|
|
245
|
+
"",
|
|
246
|
+
"4. CACHING",
|
|
247
|
+
" - Cache frequently accessed objects",
|
|
248
|
+
" - Use query result caching",
|
|
249
|
+
" - Consider materialized views for complex aggregations",
|
|
250
|
+
"",
|
|
251
|
+
"5. MONITORING",
|
|
252
|
+
" - Track slow queries in production",
|
|
253
|
+
" - Monitor database connection pool",
|
|
254
|
+
" - Alert on query performance degradation",
|
|
255
|
+
"",
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Write report
|
|
260
|
+
output = "\n".join(lines)
|
|
261
|
+
dest = ctx.workdir / "logs" / "141_orm_optimization.txt"
|
|
262
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
dest.write_text(output, encoding="utf-8")
|
|
264
|
+
|
|
265
|
+
elapsed = int(time.time() - start)
|
|
266
|
+
return StepResult(self.name, "OK", elapsed, "")
|
|
267
|
+
|
|
268
|
+
def _analyze_orm_optimization(self, root: Path) -> Dict:
|
|
269
|
+
"""Analyze ORM optimization patterns."""
|
|
270
|
+
primary_orm = None
|
|
271
|
+
django_patterns = {k: 0 for k in self.DJANGO_PATTERNS}
|
|
272
|
+
sqlalchemy_patterns = {k: 0 for k in self.SQLALCHEMY_PATTERNS}
|
|
273
|
+
|
|
274
|
+
python_files = list(root.rglob("*.py"))
|
|
275
|
+
|
|
276
|
+
for py_file in python_files:
|
|
277
|
+
if any(
|
|
278
|
+
part in py_file.parts
|
|
279
|
+
for part in ["venv", ".venv", "env", "__pycache__", "site-packages"]
|
|
280
|
+
):
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
source = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
285
|
+
|
|
286
|
+
# Detect ORM
|
|
287
|
+
if "from django.db" in source:
|
|
288
|
+
if not primary_orm:
|
|
289
|
+
primary_orm = "Django"
|
|
290
|
+
# Count Django patterns
|
|
291
|
+
for pattern_name, pattern in self.DJANGO_PATTERNS.items():
|
|
292
|
+
django_patterns[pattern_name] += len(
|
|
293
|
+
re.findall(pattern, source)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
elif "from sqlalchemy" in source:
|
|
297
|
+
if not primary_orm:
|
|
298
|
+
primary_orm = "SQLAlchemy"
|
|
299
|
+
# Count SQLAlchemy patterns
|
|
300
|
+
for pattern_name, pattern in self.SQLALCHEMY_PATTERNS.items():
|
|
301
|
+
sqlalchemy_patterns[pattern_name] += len(
|
|
302
|
+
re.findall(pattern, source)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
elif "from tortoise" in source:
|
|
306
|
+
if not primary_orm:
|
|
307
|
+
primary_orm = "Tortoise ORM"
|
|
308
|
+
|
|
309
|
+
except (OSError, UnicodeDecodeError):
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
"primary_orm": primary_orm,
|
|
314
|
+
"django": django_patterns,
|
|
315
|
+
"sqlalchemy": sqlalchemy_patterns,
|
|
316
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess # nosec B404 - Required for tool execution, paths validated
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .base import StepResult
|
|
8
|
+
from ..context import BundleContext
|
|
9
|
+
from ..tools import which
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class PipAuditStep:
|
|
14
|
+
name: str = "pip-audit"
|
|
15
|
+
outfile: str = "logs/51_pip_audit.txt"
|
|
16
|
+
|
|
17
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
18
|
+
start = time.time()
|
|
19
|
+
out = ctx.workdir / self.outfile
|
|
20
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
pip_audit = which("pip-audit")
|
|
23
|
+
if not pip_audit:
|
|
24
|
+
out.write_text(
|
|
25
|
+
"pip-audit not found; skipping (pip install pip-audit)\n",
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
)
|
|
28
|
+
return StepResult(self.name, "SKIP", 0, "missing pip-audit")
|
|
29
|
+
|
|
30
|
+
# Run pip-audit to check for known vulnerabilities
|
|
31
|
+
cmd = [pip_audit, "--desc", "--format", "columns"]
|
|
32
|
+
header = f"## PWD: {ctx.root}\n## CMD: {' '.join(cmd)}\n\n"
|
|
33
|
+
|
|
34
|
+
cp = subprocess.run( # nosec B603
|
|
35
|
+
cmd, cwd=str(ctx.root), text=True, capture_output=True, check=False
|
|
36
|
+
)
|
|
37
|
+
text = header + (cp.stdout or "") + ("\n" + cp.stderr if cp.stderr else "")
|
|
38
|
+
out.write_text(ctx.redact_text(text), encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
dur = int(time.time() - start)
|
|
41
|
+
# pip-audit exit codes: 0=no vulnerabilities, 1=vulnerabilities found
|
|
42
|
+
note = (
|
|
43
|
+
"" if cp.returncode == 0 else f"exit={cp.returncode} (vulnerable packages)"
|
|
44
|
+
)
|
|
45
|
+
return StepResult(self.name, "PASS", dur, note)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess # nosec B404 - Required for tool execution, paths validated
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .base import StepResult
|
|
8
|
+
from ..context import BundleContext
|
|
9
|
+
from ..tools import which
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class PipdeptreeStep:
|
|
14
|
+
name: str = "pipdeptree"
|
|
15
|
+
outfile: str = "meta/30_dependency_tree.txt"
|
|
16
|
+
|
|
17
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
18
|
+
start = time.time()
|
|
19
|
+
out = ctx.workdir / self.outfile
|
|
20
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
pipdeptree = which("pipdeptree")
|
|
23
|
+
if not pipdeptree:
|
|
24
|
+
out.write_text(
|
|
25
|
+
"pipdeptree not found; skipping (pip install pipdeptree)\n",
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
)
|
|
28
|
+
return StepResult(self.name, "SKIP", 0, "missing pipdeptree")
|
|
29
|
+
|
|
30
|
+
# Run pipdeptree with warnings for conflicts
|
|
31
|
+
cmd = [
|
|
32
|
+
pipdeptree,
|
|
33
|
+
"--warn",
|
|
34
|
+
"fail", # Show warnings for conflicting dependencies
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run( # nosec B603 - Using full path from which()
|
|
39
|
+
cmd,
|
|
40
|
+
cwd=ctx.root,
|
|
41
|
+
stdout=subprocess.PIPE,
|
|
42
|
+
stderr=subprocess.PIPE,
|
|
43
|
+
text=True,
|
|
44
|
+
timeout=60,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Combine stdout and stderr to capture both tree and warnings
|
|
48
|
+
output = result.stdout
|
|
49
|
+
if result.stderr:
|
|
50
|
+
output += "\n\n=== WARNINGS ===\n" + result.stderr
|
|
51
|
+
|
|
52
|
+
out.write_text(output, encoding="utf-8")
|
|
53
|
+
elapsed = int((time.time() - start) * 1000)
|
|
54
|
+
|
|
55
|
+
# pipdeptree returns 0 on success, even with warnings
|
|
56
|
+
return StepResult(self.name, "OK", elapsed, "")
|
|
57
|
+
except subprocess.TimeoutExpired:
|
|
58
|
+
out.write_text("pipdeptree timed out after 60s\n", encoding="utf-8")
|
|
59
|
+
return StepResult(self.name, "FAIL", 60000, "timeout")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
out.write_text(f"pipdeptree error: {e}\n", encoding="utf-8")
|
|
62
|
+
return StepResult(self.name, "FAIL", 0, str(e))
|