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.

Files changed (55) hide show
  1. gwc_pybundle-1.4.5.dist-info/METADATA +876 -0
  2. gwc_pybundle-1.4.5.dist-info/RECORD +55 -0
  3. gwc_pybundle-1.4.5.dist-info/WHEEL +5 -0
  4. gwc_pybundle-1.4.5.dist-info/entry_points.txt +2 -0
  5. gwc_pybundle-1.4.5.dist-info/licenses/LICENSE.md +25 -0
  6. gwc_pybundle-1.4.5.dist-info/top_level.txt +1 -0
  7. pybundle/__init__.py +0 -0
  8. pybundle/__main__.py +4 -0
  9. pybundle/cli.py +365 -0
  10. pybundle/context.py +362 -0
  11. pybundle/doctor.py +148 -0
  12. pybundle/filters.py +178 -0
  13. pybundle/manifest.py +77 -0
  14. pybundle/packaging.py +45 -0
  15. pybundle/policy.py +132 -0
  16. pybundle/profiles.py +340 -0
  17. pybundle/roadmap_model.py +42 -0
  18. pybundle/roadmap_scan.py +295 -0
  19. pybundle/root_detect.py +14 -0
  20. pybundle/runner.py +163 -0
  21. pybundle/steps/__init__.py +26 -0
  22. pybundle/steps/bandit.py +72 -0
  23. pybundle/steps/base.py +20 -0
  24. pybundle/steps/compileall.py +76 -0
  25. pybundle/steps/context_expand.py +272 -0
  26. pybundle/steps/copy_pack.py +293 -0
  27. pybundle/steps/coverage.py +101 -0
  28. pybundle/steps/cprofile_step.py +155 -0
  29. pybundle/steps/dependency_sizes.py +120 -0
  30. pybundle/steps/duplication.py +94 -0
  31. pybundle/steps/error_refs.py +204 -0
  32. pybundle/steps/handoff_md.py +167 -0
  33. pybundle/steps/import_time.py +165 -0
  34. pybundle/steps/interrogate.py +84 -0
  35. pybundle/steps/license_scan.py +96 -0
  36. pybundle/steps/line_profiler.py +108 -0
  37. pybundle/steps/memory_profile.py +173 -0
  38. pybundle/steps/mutation_testing.py +136 -0
  39. pybundle/steps/mypy.py +60 -0
  40. pybundle/steps/pip_audit.py +45 -0
  41. pybundle/steps/pipdeptree.py +61 -0
  42. pybundle/steps/pylance.py +562 -0
  43. pybundle/steps/pytest.py +66 -0
  44. pybundle/steps/radon.py +121 -0
  45. pybundle/steps/repro_md.py +161 -0
  46. pybundle/steps/rg_scans.py +78 -0
  47. pybundle/steps/roadmap.py +153 -0
  48. pybundle/steps/ruff.py +111 -0
  49. pybundle/steps/shell.py +74 -0
  50. pybundle/steps/slow_tests.py +170 -0
  51. pybundle/steps/test_flakiness.py +172 -0
  52. pybundle/steps/tree.py +116 -0
  53. pybundle/steps/unused_deps.py +112 -0
  54. pybundle/steps/vulture.py +83 -0
  55. 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))