gwc-pybundle 1.4.5__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-1.4.5.dist-info/METADATA +876 -0
- gwc_pybundle-1.4.5.dist-info/RECORD +55 -0
- gwc_pybundle-1.4.5.dist-info/WHEEL +5 -0
- gwc_pybundle-1.4.5.dist-info/entry_points.txt +2 -0
- gwc_pybundle-1.4.5.dist-info/licenses/LICENSE.md +25 -0
- gwc_pybundle-1.4.5.dist-info/top_level.txt +1 -0
- pybundle/__init__.py +0 -0
- pybundle/__main__.py +4 -0
- pybundle/cli.py +365 -0
- pybundle/context.py +362 -0
- pybundle/doctor.py +148 -0
- pybundle/filters.py +178 -0
- pybundle/manifest.py +77 -0
- pybundle/packaging.py +45 -0
- pybundle/policy.py +132 -0
- pybundle/profiles.py +340 -0
- pybundle/roadmap_model.py +42 -0
- pybundle/roadmap_scan.py +295 -0
- pybundle/root_detect.py +14 -0
- pybundle/runner.py +163 -0
- pybundle/steps/__init__.py +26 -0
- pybundle/steps/bandit.py +72 -0
- pybundle/steps/base.py +20 -0
- pybundle/steps/compileall.py +76 -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 +155 -0
- pybundle/steps/dependency_sizes.py +120 -0
- pybundle/steps/duplication.py +94 -0
- pybundle/steps/error_refs.py +204 -0
- pybundle/steps/handoff_md.py +167 -0
- pybundle/steps/import_time.py +165 -0
- pybundle/steps/interrogate.py +84 -0
- pybundle/steps/license_scan.py +96 -0
- pybundle/steps/line_profiler.py +108 -0
- pybundle/steps/memory_profile.py +173 -0
- pybundle/steps/mutation_testing.py +136 -0
- pybundle/steps/mypy.py +60 -0
- pybundle/steps/pip_audit.py +45 -0
- pybundle/steps/pipdeptree.py +61 -0
- pybundle/steps/pylance.py +562 -0
- pybundle/steps/pytest.py +66 -0
- pybundle/steps/radon.py +121 -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 +111 -0
- pybundle/steps/shell.py +74 -0
- pybundle/steps/slow_tests.py +170 -0
- pybundle/steps/test_flakiness.py +172 -0
- pybundle/steps/tree.py +116 -0
- pybundle/steps/unused_deps.py +112 -0
- pybundle/steps/vulture.py +83 -0
- pybundle/tools.py +63 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory profiling with tracemalloc - Milestone 3 (v1.4.0)
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .base import StepResult
|
|
12
|
+
from ..context import BundleContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class MemoryProfileStep:
|
|
17
|
+
"""
|
|
18
|
+
Memory profiling using tracemalloc to identify memory-consuming operations.
|
|
19
|
+
|
|
20
|
+
Outputs:
|
|
21
|
+
- logs/62_memory_profile.txt: Top memory-consuming functions and allocations
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name: str = "memory_profile"
|
|
25
|
+
|
|
26
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
27
|
+
start = time.time()
|
|
28
|
+
|
|
29
|
+
if ctx.options.no_profile or not ctx.options.profile_memory:
|
|
30
|
+
return StepResult(self.name, "SKIP", 0, "memory profiling not enabled")
|
|
31
|
+
|
|
32
|
+
# Require pytest for memory profiling
|
|
33
|
+
if not ctx.tools.pytest:
|
|
34
|
+
return StepResult(self.name, "SKIP", 0, "pytest not found")
|
|
35
|
+
|
|
36
|
+
tests_dir = ctx.root / "tests"
|
|
37
|
+
if not tests_dir.is_dir():
|
|
38
|
+
return StepResult(self.name, "SKIP", 0, "no tests/ directory")
|
|
39
|
+
|
|
40
|
+
ctx.emit(" Running memory profiling on test suite")
|
|
41
|
+
|
|
42
|
+
# Create a temporary script to run pytest with tracemalloc
|
|
43
|
+
profile_script = self._create_profile_script(ctx.root)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Run the profiling script
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
[str(ctx.tools.python), str(profile_script)],
|
|
49
|
+
cwd=ctx.root,
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
timeout=300 # 5 minute timeout
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Write output
|
|
56
|
+
output_file = ctx.workdir / "logs" / "62_memory_profile.txt"
|
|
57
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
with output_file.open("w") as f:
|
|
60
|
+
f.write("=" * 70 + "\n")
|
|
61
|
+
f.write("MEMORY PROFILING (tracemalloc)\n")
|
|
62
|
+
f.write("=" * 70 + "\n\n")
|
|
63
|
+
|
|
64
|
+
if result.returncode == 0:
|
|
65
|
+
f.write(result.stdout)
|
|
66
|
+
if result.stderr:
|
|
67
|
+
f.write("\n\nTest output:\n")
|
|
68
|
+
f.write(result.stderr)
|
|
69
|
+
else:
|
|
70
|
+
f.write("Memory profiling failed\n\n")
|
|
71
|
+
f.write("STDOUT:\n")
|
|
72
|
+
f.write(result.stdout)
|
|
73
|
+
f.write("\n\nSTDERR:\n")
|
|
74
|
+
f.write(result.stderr)
|
|
75
|
+
|
|
76
|
+
elapsed = int((time.time() - start) * 1000)
|
|
77
|
+
if result.returncode == 0:
|
|
78
|
+
return StepResult(self.name, "OK", elapsed)
|
|
79
|
+
else:
|
|
80
|
+
return StepResult(self.name, "FAIL", elapsed, f"exit {result.returncode}")
|
|
81
|
+
|
|
82
|
+
except subprocess.TimeoutExpired:
|
|
83
|
+
elapsed = int((time.time() - start) * 1000)
|
|
84
|
+
return StepResult(self.name, "FAIL", elapsed, "timeout")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
elapsed = int((time.time() - start) * 1000)
|
|
87
|
+
return StepResult(self.name, "FAIL", elapsed, str(e))
|
|
88
|
+
finally:
|
|
89
|
+
# Clean up temporary script
|
|
90
|
+
if profile_script.exists():
|
|
91
|
+
profile_script.unlink()
|
|
92
|
+
|
|
93
|
+
def _create_profile_script(self, root: Path) -> Path:
|
|
94
|
+
"""Create a temporary Python script that runs pytest with tracemalloc"""
|
|
95
|
+
script_path = root / ".pybundle_memory_profile.py"
|
|
96
|
+
|
|
97
|
+
script_content = '''"""Temporary memory profiling script for pybundle"""
|
|
98
|
+
import tracemalloc
|
|
99
|
+
import sys
|
|
100
|
+
import pytest
|
|
101
|
+
|
|
102
|
+
def format_size(size_bytes):
|
|
103
|
+
"""Format bytes as human-readable string"""
|
|
104
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
105
|
+
if size_bytes < 1024.0:
|
|
106
|
+
return f"{size_bytes:.2f} {unit}"
|
|
107
|
+
size_bytes /= 1024.0
|
|
108
|
+
return f"{size_bytes:.2f} TB"
|
|
109
|
+
|
|
110
|
+
# Start tracing
|
|
111
|
+
tracemalloc.start()
|
|
112
|
+
|
|
113
|
+
# Record initial snapshot
|
|
114
|
+
snapshot1 = tracemalloc.take_snapshot()
|
|
115
|
+
|
|
116
|
+
# Run pytest
|
|
117
|
+
exit_code = pytest.main(["-q"])
|
|
118
|
+
|
|
119
|
+
# Take final snapshot
|
|
120
|
+
snapshot2 = tracemalloc.take_snapshot()
|
|
121
|
+
|
|
122
|
+
# Get traced memory
|
|
123
|
+
current, peak = tracemalloc.get_traced_memory()
|
|
124
|
+
|
|
125
|
+
# Stop tracing
|
|
126
|
+
tracemalloc.stop()
|
|
127
|
+
|
|
128
|
+
# Analyze differences
|
|
129
|
+
print("=" * 70)
|
|
130
|
+
print("MEMORY ALLOCATION SUMMARY")
|
|
131
|
+
print("=" * 70)
|
|
132
|
+
print()
|
|
133
|
+
print(f"Peak memory usage: {format_size(peak)}")
|
|
134
|
+
print()
|
|
135
|
+
|
|
136
|
+
# Top allocations
|
|
137
|
+
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
|
|
138
|
+
|
|
139
|
+
print("TOP 30 MEMORY ALLOCATIONS (by increase):")
|
|
140
|
+
print("-" * 70)
|
|
141
|
+
print(f"{'Size':<12} {'Count':<8} {'Location'}")
|
|
142
|
+
print("-" * 70)
|
|
143
|
+
|
|
144
|
+
for stat in top_stats[:30]:
|
|
145
|
+
print(f"{format_size(stat.size):<12} {stat.count:<8} {stat.traceback}")
|
|
146
|
+
|
|
147
|
+
# Top by current size
|
|
148
|
+
print()
|
|
149
|
+
print("=" * 70)
|
|
150
|
+
print("TOP 30 MEMORY CONSUMERS (by total size):")
|
|
151
|
+
print("-" * 70)
|
|
152
|
+
print(f"{'Size':<12} {'Count':<8} {'Location'}")
|
|
153
|
+
print("-" * 70)
|
|
154
|
+
|
|
155
|
+
current_stats = snapshot2.statistics('lineno')
|
|
156
|
+
for stat in current_stats[:30]:
|
|
157
|
+
print(f"{format_size(stat.size):<12} {stat.count:<8} {stat.traceback}")
|
|
158
|
+
|
|
159
|
+
print()
|
|
160
|
+
print("=" * 70)
|
|
161
|
+
print("RECOMMENDATIONS:")
|
|
162
|
+
print("- Review functions with large memory allocations")
|
|
163
|
+
print("- Check for memory leaks in repeatedly allocated objects")
|
|
164
|
+
print("- Consider using generators for large data processing")
|
|
165
|
+
print("=" * 70)
|
|
166
|
+
|
|
167
|
+
sys.exit(exit_code)
|
|
168
|
+
'''
|
|
169
|
+
|
|
170
|
+
with script_path.open("w") as f:
|
|
171
|
+
f.write(script_content)
|
|
172
|
+
|
|
173
|
+
return script_path
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mutation testing with mutmut - Milestone 4 (v1.4.1)
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .base import StepResult
|
|
12
|
+
from ..context import BundleContext
|
|
13
|
+
from ..tools import which
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class MutationTestingStep:
|
|
18
|
+
"""
|
|
19
|
+
Mutation testing to measure test suite effectiveness.
|
|
20
|
+
|
|
21
|
+
EXPENSIVE: Disabled by default. Run many test executions with code mutations.
|
|
22
|
+
|
|
23
|
+
Outputs:
|
|
24
|
+
- logs/72_mutation_testing.txt: Mutation testing results
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
name: str = "mutation_testing"
|
|
28
|
+
|
|
29
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
30
|
+
start = time.time()
|
|
31
|
+
|
|
32
|
+
# Only run if explicitly enabled (very slow!)
|
|
33
|
+
if not ctx.options.enable_mutation_testing:
|
|
34
|
+
return StepResult(self.name, "SKIP", 0, "mutation testing not enabled (slow!)")
|
|
35
|
+
|
|
36
|
+
# Check for mutmut
|
|
37
|
+
mutmut = which("mutmut")
|
|
38
|
+
if not mutmut:
|
|
39
|
+
output_file = ctx.workdir / "logs" / "72_mutation_testing.txt"
|
|
40
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
output_file.write_text(
|
|
42
|
+
"mutmut not found; install with: pip install mutmut\n",
|
|
43
|
+
encoding="utf-8"
|
|
44
|
+
)
|
|
45
|
+
return StepResult(self.name, "SKIP", 0, "mutmut not installed")
|
|
46
|
+
|
|
47
|
+
if not ctx.tools.pytest:
|
|
48
|
+
return StepResult(self.name, "SKIP", 0, "pytest not found")
|
|
49
|
+
|
|
50
|
+
tests_dir = ctx.root / "tests"
|
|
51
|
+
if not tests_dir.is_dir():
|
|
52
|
+
return StepResult(self.name, "SKIP", 0, "no tests/ directory")
|
|
53
|
+
|
|
54
|
+
ctx.emit(" ⚠️ Running mutation testing (this may take several minutes)...")
|
|
55
|
+
|
|
56
|
+
output_file = ctx.workdir / "logs" / "72_mutation_testing.txt"
|
|
57
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Run mutmut
|
|
61
|
+
# First, run mutmut run to generate mutations
|
|
62
|
+
run_result = subprocess.run(
|
|
63
|
+
[mutmut, "run", "--paths-to-mutate", "."],
|
|
64
|
+
cwd=ctx.root,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
timeout=600 # 10 minute timeout (mutation testing is SLOW)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Then get results summary
|
|
71
|
+
results_result = subprocess.run(
|
|
72
|
+
[mutmut, "results"],
|
|
73
|
+
cwd=ctx.root,
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
timeout=60
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Generate report
|
|
80
|
+
with output_file.open("w") as f:
|
|
81
|
+
f.write("=" * 70 + "\n")
|
|
82
|
+
f.write("MUTATION TESTING (mutmut)\n")
|
|
83
|
+
f.write("=" * 70 + "\n")
|
|
84
|
+
f.write("⚠️ WARNING: Mutation testing is VERY SLOW\n")
|
|
85
|
+
f.write("=" * 70 + "\n\n")
|
|
86
|
+
|
|
87
|
+
f.write("MUTATION RUN OUTPUT:\n")
|
|
88
|
+
f.write("-" * 70 + "\n")
|
|
89
|
+
f.write(run_result.stdout)
|
|
90
|
+
if run_result.stderr:
|
|
91
|
+
f.write("\nErrors:\n")
|
|
92
|
+
f.write(run_result.stderr)
|
|
93
|
+
|
|
94
|
+
f.write("\n" + "=" * 70 + "\n")
|
|
95
|
+
f.write("MUTATION RESULTS SUMMARY:\n")
|
|
96
|
+
f.write("-" * 70 + "\n")
|
|
97
|
+
f.write(results_result.stdout)
|
|
98
|
+
if results_result.stderr:
|
|
99
|
+
f.write("\nErrors:\n")
|
|
100
|
+
f.write(results_result.stderr)
|
|
101
|
+
|
|
102
|
+
f.write("\n" + "=" * 70 + "\n")
|
|
103
|
+
f.write("INTERPRETATION:\n")
|
|
104
|
+
f.write("-" * 70 + "\n")
|
|
105
|
+
f.write("- Killed mutations: Your tests caught the bug (GOOD!)\n")
|
|
106
|
+
f.write("- Survived mutations: Your tests missed the bug (BAD!)\n")
|
|
107
|
+
f.write("- Timeout/Suspicious: Tests took too long or behaved oddly\n")
|
|
108
|
+
f.write("\n")
|
|
109
|
+
f.write("Mutation Score = Killed / (Killed + Survived + Timeout)\n")
|
|
110
|
+
f.write("Target: >80% mutation score for well-tested code\n")
|
|
111
|
+
f.write("\n")
|
|
112
|
+
f.write("To see specific survived mutations:\n")
|
|
113
|
+
f.write(" mutmut show <id>\n")
|
|
114
|
+
f.write("\n")
|
|
115
|
+
f.write("=" * 70 + "\n")
|
|
116
|
+
f.write("RECOMMENDATIONS:\n")
|
|
117
|
+
f.write("- Add tests for survived mutations\n")
|
|
118
|
+
f.write("- Focus on edge cases and boundary conditions\n")
|
|
119
|
+
f.write("- Improve assertion quality (not just 'assert result')\n")
|
|
120
|
+
|
|
121
|
+
elapsed = int((time.time() - start) * 1000)
|
|
122
|
+
|
|
123
|
+
if run_result.returncode == 0:
|
|
124
|
+
return StepResult(self.name, "OK", elapsed)
|
|
125
|
+
else:
|
|
126
|
+
return StepResult(self.name, "FAIL", elapsed, f"exit {run_result.returncode}")
|
|
127
|
+
|
|
128
|
+
except subprocess.TimeoutExpired:
|
|
129
|
+
elapsed = int((time.time() - start) * 1000)
|
|
130
|
+
with output_file.open("w") as f:
|
|
131
|
+
f.write("Mutation testing timed out after 10 minutes\n")
|
|
132
|
+
f.write("Consider testing a smaller subset or using --paths-to-mutate\n")
|
|
133
|
+
return StepResult(self.name, "FAIL", elapsed, "timeout")
|
|
134
|
+
except Exception as e:
|
|
135
|
+
elapsed = int((time.time() - start) * 1000)
|
|
136
|
+
return StepResult(self.name, "FAIL", elapsed, str(e))
|
pybundle/steps/mypy.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .base import StepResult
|
|
9
|
+
from ..context import BundleContext
|
|
10
|
+
from ..tools import which
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _has_mypy_config(root: Path) -> bool:
|
|
14
|
+
if (root / "mypy.ini").is_file():
|
|
15
|
+
return True
|
|
16
|
+
if (root / "setup.cfg").is_file():
|
|
17
|
+
return True
|
|
18
|
+
if (root / "pyproject.toml").is_file():
|
|
19
|
+
# we don't parse TOML here; presence is enough for v1
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class MypyStep:
|
|
26
|
+
name: str = "mypy"
|
|
27
|
+
target: str = "pybundle"
|
|
28
|
+
outfile: str = "logs/33_mypy.txt"
|
|
29
|
+
|
|
30
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
31
|
+
start = time.time()
|
|
32
|
+
out = ctx.workdir / self.outfile
|
|
33
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
mypy = which("mypy")
|
|
36
|
+
if not mypy:
|
|
37
|
+
out.write_text(
|
|
38
|
+
"mypy not found; skipping (pip install mypy)\n", encoding="utf-8"
|
|
39
|
+
)
|
|
40
|
+
return StepResult(self.name, "SKIP", 0, "missing mypy")
|
|
41
|
+
|
|
42
|
+
if not _has_mypy_config(ctx.root):
|
|
43
|
+
out.write_text(
|
|
44
|
+
"no mypy config detected (mypy.ini/setup.cfg/pyproject.toml); skipping\n",
|
|
45
|
+
encoding="utf-8",
|
|
46
|
+
)
|
|
47
|
+
return StepResult(self.name, "SKIP", 0, "no config")
|
|
48
|
+
|
|
49
|
+
cmd = [mypy, "--exclude", "^artifacts/", self.target]
|
|
50
|
+
header = f"## PWD: {ctx.root}\n## CMD: {' '.join(cmd)}\n\n"
|
|
51
|
+
|
|
52
|
+
cp = subprocess.run( # nosec B603
|
|
53
|
+
cmd, cwd=str(ctx.root), text=True, capture_output=True, check=False
|
|
54
|
+
)
|
|
55
|
+
text = header + (cp.stdout or "") + ("\n" + cp.stderr if cp.stderr else "")
|
|
56
|
+
out.write_text(ctx.redact_text(text), encoding="utf-8")
|
|
57
|
+
|
|
58
|
+
dur = int(time.time() - start)
|
|
59
|
+
note = "" if cp.returncode == 0 else f"exit={cp.returncode} (type findings)"
|
|
60
|
+
return StepResult(self.name, "PASS", dur, note)
|
|
@@ -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,61 @@
|
|
|
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", "fail", # Show warnings for conflicting dependencies
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
result = subprocess.run( # nosec B603 - Using full path from which()
|
|
38
|
+
cmd,
|
|
39
|
+
cwd=ctx.root,
|
|
40
|
+
stdout=subprocess.PIPE,
|
|
41
|
+
stderr=subprocess.PIPE,
|
|
42
|
+
text=True,
|
|
43
|
+
timeout=60,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Combine stdout and stderr to capture both tree and warnings
|
|
47
|
+
output = result.stdout
|
|
48
|
+
if result.stderr:
|
|
49
|
+
output += "\n\n=== WARNINGS ===\n" + result.stderr
|
|
50
|
+
|
|
51
|
+
out.write_text(output, encoding="utf-8")
|
|
52
|
+
elapsed = int((time.time() - start) * 1000)
|
|
53
|
+
|
|
54
|
+
# pipdeptree returns 0 on success, even with warnings
|
|
55
|
+
return StepResult(self.name, "OK", elapsed, None)
|
|
56
|
+
except subprocess.TimeoutExpired:
|
|
57
|
+
out.write_text("pipdeptree timed out after 60s\n", encoding="utf-8")
|
|
58
|
+
return StepResult(self.name, "FAIL", 60000, "timeout")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
out.write_text(f"pipdeptree error: {e}\n", encoding="utf-8")
|
|
61
|
+
return StepResult(self.name, "FAIL", 0, str(e))
|