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
pybundle/context.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field, asdict
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterable, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from .tools import which
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .steps.base import StepResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def fmt_tool(path: str | None) -> str:
|
|
17
|
+
if path:
|
|
18
|
+
return path
|
|
19
|
+
return "\x1b[31m<missing>\x1b[0m"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Tooling:
|
|
24
|
+
git: str | None
|
|
25
|
+
python: str | None
|
|
26
|
+
pip: str | None
|
|
27
|
+
zip: str | None
|
|
28
|
+
tar: str | None
|
|
29
|
+
uname: str | None
|
|
30
|
+
|
|
31
|
+
# analysis/debug tools
|
|
32
|
+
ruff: str | None
|
|
33
|
+
mypy: str | None
|
|
34
|
+
pytest: str | None
|
|
35
|
+
rg: str | None
|
|
36
|
+
tree: str | None
|
|
37
|
+
npm: str | None
|
|
38
|
+
|
|
39
|
+
# code quality tools (v1.3.0)
|
|
40
|
+
vulture: str | None
|
|
41
|
+
radon: str | None
|
|
42
|
+
interrogate: str | None
|
|
43
|
+
pylint: str | None
|
|
44
|
+
|
|
45
|
+
# dependency analysis tools (v1.3.1)
|
|
46
|
+
pipdeptree: str | None
|
|
47
|
+
pip_licenses: str | None
|
|
48
|
+
|
|
49
|
+
# performance profiling tools (v1.4.0)
|
|
50
|
+
line_profiler: str | None
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def detect(strict_paths: bool = False) -> "Tooling":
|
|
54
|
+
"""Detect available tools, optionally enforcing trusted paths.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
strict_paths: If True, only accept tools in trusted system directories
|
|
58
|
+
"""
|
|
59
|
+
return Tooling(
|
|
60
|
+
git=which("git", strict=strict_paths),
|
|
61
|
+
python=which("python", strict=strict_paths)
|
|
62
|
+
or which("python3", strict=strict_paths),
|
|
63
|
+
pip=which("pip", strict=strict_paths) or which("pip3", strict=strict_paths),
|
|
64
|
+
zip=which("zip", strict=strict_paths),
|
|
65
|
+
tar=which("tar", strict=strict_paths),
|
|
66
|
+
uname=which("uname", strict=strict_paths),
|
|
67
|
+
ruff=which("ruff", strict=strict_paths),
|
|
68
|
+
mypy=which("mypy", strict=strict_paths),
|
|
69
|
+
pytest=which("pytest", strict=strict_paths),
|
|
70
|
+
rg=which("rg", strict=strict_paths),
|
|
71
|
+
tree=which("tree", strict=strict_paths),
|
|
72
|
+
npm=which("npm", strict=strict_paths),
|
|
73
|
+
vulture=which("vulture", strict=strict_paths),
|
|
74
|
+
radon=which("radon", strict=strict_paths),
|
|
75
|
+
interrogate=which("interrogate", strict=strict_paths),
|
|
76
|
+
pylint=which("pylint", strict=strict_paths),
|
|
77
|
+
pipdeptree=which("pipdeptree", strict=strict_paths),
|
|
78
|
+
pip_licenses=which("pip-licenses", strict=strict_paths),
|
|
79
|
+
line_profiler=which("kernprof", strict=strict_paths),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class RunOptions:
|
|
85
|
+
no_ruff: bool | None = None
|
|
86
|
+
no_mypy: bool | None = None
|
|
87
|
+
no_pylance: bool | None = None
|
|
88
|
+
no_pytest: bool | None = None
|
|
89
|
+
no_bandit: bool | None = None
|
|
90
|
+
no_pip_audit: bool | None = None
|
|
91
|
+
no_coverage: bool | None = None
|
|
92
|
+
no_rg: bool | None = None
|
|
93
|
+
no_error_refs: bool | None = None
|
|
94
|
+
no_context: bool | None = None
|
|
95
|
+
no_compileall: bool | None = None
|
|
96
|
+
|
|
97
|
+
# code quality tools (v1.3.0)
|
|
98
|
+
no_vulture: bool | None = None
|
|
99
|
+
no_radon: bool | None = None
|
|
100
|
+
no_interrogate: bool | None = None
|
|
101
|
+
no_duplication: bool | None = None
|
|
102
|
+
|
|
103
|
+
# dependency analysis tools (v1.3.1)
|
|
104
|
+
no_pipdeptree: bool | None = None
|
|
105
|
+
no_unused_deps: bool | None = None
|
|
106
|
+
no_license_scan: bool | None = None
|
|
107
|
+
no_dependency_sizes: bool | None = None
|
|
108
|
+
|
|
109
|
+
# performance profiling (v1.4.0)
|
|
110
|
+
no_profile: bool | None = None
|
|
111
|
+
profile_entry_point: str | None = None
|
|
112
|
+
profile_memory: bool = True # v1.4.2: enabled by default
|
|
113
|
+
enable_line_profiler: bool = False # requires @profile decorators
|
|
114
|
+
|
|
115
|
+
# test quality & coverage (v1.4.1)
|
|
116
|
+
test_flakiness_runs: int = 3
|
|
117
|
+
slow_test_threshold: float = 1.0
|
|
118
|
+
enable_mutation_testing: bool = False
|
|
119
|
+
|
|
120
|
+
strict_paths: bool = False # Enforce trusted path validation
|
|
121
|
+
|
|
122
|
+
ruff_target: str = "."
|
|
123
|
+
mypy_target: str = "."
|
|
124
|
+
pytest_args: list[str] = field(default_factory=lambda: ["-q"])
|
|
125
|
+
|
|
126
|
+
error_max_files: int = 250
|
|
127
|
+
context_depth: int = 2
|
|
128
|
+
context_max_files: int = 600
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class BundleContext:
|
|
133
|
+
root: Path
|
|
134
|
+
options: RunOptions
|
|
135
|
+
outdir: Path
|
|
136
|
+
profile_name: str
|
|
137
|
+
ts: str
|
|
138
|
+
workdir: Path
|
|
139
|
+
srcdir: Path
|
|
140
|
+
logdir: Path
|
|
141
|
+
metadir: Path
|
|
142
|
+
runlog: Path
|
|
143
|
+
summary_json: Path
|
|
144
|
+
manifest_json: Path
|
|
145
|
+
archive_format: str
|
|
146
|
+
name_prefix: str
|
|
147
|
+
strict: bool
|
|
148
|
+
redact: bool
|
|
149
|
+
keep_workdir: bool
|
|
150
|
+
tools: Tooling
|
|
151
|
+
results: list["StepResult"] = field(default_factory=list)
|
|
152
|
+
command_used: str = ""
|
|
153
|
+
json_mode: bool = False
|
|
154
|
+
archive_path: Path | None = None
|
|
155
|
+
duration_ms: int | None = None
|
|
156
|
+
|
|
157
|
+
def have(self, cmd: str) -> bool:
|
|
158
|
+
return getattr(self.tools, cmd, None) is not None
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def utc_ts() -> str:
|
|
162
|
+
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def create(
|
|
166
|
+
cls,
|
|
167
|
+
*,
|
|
168
|
+
root: Path,
|
|
169
|
+
options: RunOptions | None = None,
|
|
170
|
+
outdir: Path,
|
|
171
|
+
profile_name: str,
|
|
172
|
+
archive_format: str,
|
|
173
|
+
name_prefix: str | None,
|
|
174
|
+
strict: bool,
|
|
175
|
+
redact: bool,
|
|
176
|
+
keep_workdir: bool,
|
|
177
|
+
json_mode: bool = False,
|
|
178
|
+
) -> "BundleContext":
|
|
179
|
+
ts = cls.utc_ts()
|
|
180
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
|
|
182
|
+
workdir = outdir / f"pybundle_{profile_name}_{ts}"
|
|
183
|
+
srcdir = workdir / "src"
|
|
184
|
+
logdir = workdir / "logs"
|
|
185
|
+
metadir = workdir / "meta"
|
|
186
|
+
|
|
187
|
+
srcdir.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
logdir.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
metadir.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
|
|
191
|
+
runlog = workdir / "RUN_LOG.txt"
|
|
192
|
+
summary_json = workdir / "SUMMARY.json"
|
|
193
|
+
manifest_json = workdir / "MANIFEST.json"
|
|
194
|
+
|
|
195
|
+
options = options or RunOptions()
|
|
196
|
+
tools = Tooling.detect(strict_paths=options.strict_paths)
|
|
197
|
+
prefix = name_prefix or f"pybundle_{profile_name}_{ts}"
|
|
198
|
+
|
|
199
|
+
return cls(
|
|
200
|
+
root=root,
|
|
201
|
+
options=options,
|
|
202
|
+
outdir=outdir,
|
|
203
|
+
profile_name=profile_name,
|
|
204
|
+
ts=ts,
|
|
205
|
+
workdir=workdir,
|
|
206
|
+
srcdir=srcdir,
|
|
207
|
+
logdir=logdir,
|
|
208
|
+
metadir=metadir,
|
|
209
|
+
runlog=runlog,
|
|
210
|
+
summary_json=summary_json,
|
|
211
|
+
manifest_json=manifest_json,
|
|
212
|
+
archive_format=archive_format,
|
|
213
|
+
name_prefix=prefix,
|
|
214
|
+
strict=strict,
|
|
215
|
+
redact=redact,
|
|
216
|
+
keep_workdir=keep_workdir,
|
|
217
|
+
tools=tools,
|
|
218
|
+
json_mode=json_mode,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def rel(self, p: Path) -> str:
|
|
222
|
+
try:
|
|
223
|
+
return str(p.relative_to(self.root))
|
|
224
|
+
except Exception:
|
|
225
|
+
return str(p)
|
|
226
|
+
|
|
227
|
+
def redact_text(self, text: str) -> str:
|
|
228
|
+
if not self.redact:
|
|
229
|
+
return text
|
|
230
|
+
# Minimal default redaction rules (you can expand with a rules file later)
|
|
231
|
+
rules: Iterable[tuple[str, str]] = [
|
|
232
|
+
(
|
|
233
|
+
r"(?i)(api[_-]?key)\s*[:=]\s*['\"]?([A-Za-z0-9_\-]{10,})",
|
|
234
|
+
r"\1=<REDACTED>",
|
|
235
|
+
),
|
|
236
|
+
(r"(?i)(token)\s*[:=]\s*['\"]?([A-Za-z0-9_\-\.]{10,})", r"\1=<REDACTED>"),
|
|
237
|
+
(r"(?i)(password|passwd|pwd)\s*[:=]\s*['\"]?([^'\"\s]+)", r"\1=<REDACTED>"),
|
|
238
|
+
(r"(?i)(dsn)\s*[:=]\s*['\"]?([^'\"\s]+)", r"\1=<REDACTED>"),
|
|
239
|
+
]
|
|
240
|
+
out = text
|
|
241
|
+
for pat, repl in rules:
|
|
242
|
+
out = re.sub(pat, repl, out)
|
|
243
|
+
return out
|
|
244
|
+
|
|
245
|
+
def write_runlog(self, line: str) -> None:
|
|
246
|
+
self.runlog.parent.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
with self.runlog.open("a", encoding="utf-8") as f:
|
|
248
|
+
f.write(line.rstrip() + "\n")
|
|
249
|
+
|
|
250
|
+
def print_doctor(self, profile) -> None:
|
|
251
|
+
from .doctor import plan_for_profile, print_tool_info
|
|
252
|
+
|
|
253
|
+
print(f"Root: {self.root}")
|
|
254
|
+
print(f"Out: {self.outdir}\n")
|
|
255
|
+
|
|
256
|
+
# Enhanced tool information with security validation
|
|
257
|
+
print_tool_info(self)
|
|
258
|
+
print()
|
|
259
|
+
|
|
260
|
+
# Options (super useful)
|
|
261
|
+
print("Options:")
|
|
262
|
+
o = self.options
|
|
263
|
+
print(f" strict_paths: {o.strict_paths}")
|
|
264
|
+
print(f" ruff_target: {o.ruff_target}")
|
|
265
|
+
print(f" mypy_target: {o.mypy_target}")
|
|
266
|
+
print(f" pytest_args: {' '.join(o.pytest_args)}")
|
|
267
|
+
print(f" no_ruff: {o.no_ruff}")
|
|
268
|
+
print(f" no_mypy: {o.no_mypy}")
|
|
269
|
+
print(f" no_pylance: {o.no_pylance}")
|
|
270
|
+
print(f" no_pytest: {o.no_pytest}")
|
|
271
|
+
print(f" no_bandit: {o.no_bandit}")
|
|
272
|
+
print(f" no_pip_audit: {o.no_pip_audit}")
|
|
273
|
+
print(f" no_coverage: {o.no_coverage}")
|
|
274
|
+
print(f" no_rg: {o.no_rg}")
|
|
275
|
+
print(f" no_error_refs: {o.no_error_refs}")
|
|
276
|
+
print(f" no_context: {o.no_context}")
|
|
277
|
+
print(f" error_max_files: {o.error_max_files}")
|
|
278
|
+
print(f" context_depth: {o.context_depth}")
|
|
279
|
+
print(f" context_max_files: {o.context_max_files}")
|
|
280
|
+
print()
|
|
281
|
+
|
|
282
|
+
# Plan
|
|
283
|
+
plan = plan_for_profile(self, profile)
|
|
284
|
+
|
|
285
|
+
print(f"Plan ({profile.name}):")
|
|
286
|
+
for item in plan:
|
|
287
|
+
out = f" -> {item.out_rel}" if item.out_rel else ""
|
|
288
|
+
if item.status == "RUN":
|
|
289
|
+
print(f" RUN {item.name:<28}{out}")
|
|
290
|
+
else:
|
|
291
|
+
why = f" ({item.reason})" if item.reason else ""
|
|
292
|
+
print(f" SKIP {item.name:<28}{out}{why}")
|
|
293
|
+
|
|
294
|
+
def doctor_report(self, profile) -> dict:
|
|
295
|
+
from .doctor import plan_for_profile
|
|
296
|
+
|
|
297
|
+
plan = plan_for_profile(self, profile)
|
|
298
|
+
|
|
299
|
+
tools = {}
|
|
300
|
+
for k, v in asdict(self.tools).items():
|
|
301
|
+
tools[k] = {"present": v is not None, "path": v}
|
|
302
|
+
|
|
303
|
+
o = self.options
|
|
304
|
+
return {
|
|
305
|
+
"status": "ok",
|
|
306
|
+
"command": "doctor",
|
|
307
|
+
"profile": profile.name,
|
|
308
|
+
"root": str(self.root),
|
|
309
|
+
"outdir": str(self.outdir),
|
|
310
|
+
"tools": tools,
|
|
311
|
+
"options": {
|
|
312
|
+
"ruff_target": o.ruff_target,
|
|
313
|
+
"mypy_target": o.mypy_target,
|
|
314
|
+
"pytest_args": list(o.pytest_args),
|
|
315
|
+
"no_ruff": o.no_ruff,
|
|
316
|
+
"no_mypy": o.no_mypy,
|
|
317
|
+
"no_pylance": o.no_pylance,
|
|
318
|
+
"no_pytest": o.no_pytest,
|
|
319
|
+
"no_bandit": o.no_bandit,
|
|
320
|
+
"no_pip_audit": o.no_pip_audit,
|
|
321
|
+
"no_coverage": o.no_coverage,
|
|
322
|
+
"no_rg": o.no_rg,
|
|
323
|
+
"no_error_refs": o.no_error_refs,
|
|
324
|
+
"no_context": o.no_context,
|
|
325
|
+
"no_vulture": o.no_vulture,
|
|
326
|
+
"no_radon": o.no_radon,
|
|
327
|
+
"no_interrogate": o.no_interrogate,
|
|
328
|
+
"no_duplication": o.no_duplication,
|
|
329
|
+
"no_pipdeptree": o.no_pipdeptree,
|
|
330
|
+
"no_unused_deps": o.no_unused_deps,
|
|
331
|
+
"no_license_scan": o.no_license_scan,
|
|
332
|
+
"no_dependency_sizes": o.no_dependency_sizes,
|
|
333
|
+
"no_profile": o.no_profile,
|
|
334
|
+
"profile_entry_point": o.profile_entry_point,
|
|
335
|
+
"profile_memory": o.profile_memory,
|
|
336
|
+
"enable_line_profiler": o.enable_line_profiler,
|
|
337
|
+
"test_flakiness_runs": o.test_flakiness_runs,
|
|
338
|
+
"slow_test_threshold": o.slow_test_threshold,
|
|
339
|
+
"enable_mutation_testing": o.enable_mutation_testing,
|
|
340
|
+
"error_max_files": o.error_max_files,
|
|
341
|
+
"context_depth": o.context_depth,
|
|
342
|
+
"context_max_files": o.context_max_files,
|
|
343
|
+
},
|
|
344
|
+
"plan": [
|
|
345
|
+
{
|
|
346
|
+
"name": item.name,
|
|
347
|
+
"status": item.status, # "RUN" or "SKIP"
|
|
348
|
+
"out_rel": item.out_rel,
|
|
349
|
+
"reason": item.reason,
|
|
350
|
+
}
|
|
351
|
+
for item in plan
|
|
352
|
+
],
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
def emit(self, msg: str) -> None:
|
|
356
|
+
"""Human output only."""
|
|
357
|
+
if not self.json_mode:
|
|
358
|
+
print(msg)
|
|
359
|
+
|
|
360
|
+
def emit_json(self, payload: dict) -> None:
|
|
361
|
+
"""JSON output only (stdout contract)."""
|
|
362
|
+
print(json.dumps(payload, ensure_ascii=False))
|
pybundle/doctor.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable, TypeVar
|
|
5
|
+
|
|
6
|
+
from .steps.shell import ShellStep
|
|
7
|
+
from .steps.ruff import RuffCheckStep, RuffFormatCheckStep
|
|
8
|
+
from .steps.mypy import MypyStep
|
|
9
|
+
from .steps.pytest import PytestStep
|
|
10
|
+
from .steps.rg_scans import RipgrepScanStep
|
|
11
|
+
from .tools import is_path_trusted
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PlanItem:
|
|
18
|
+
name: str
|
|
19
|
+
status: str # "RUN" | "SKIP"
|
|
20
|
+
out_rel: str | None
|
|
21
|
+
reason: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
EvalFn = Callable[[Any, T], PlanItem]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _out(step: Any) -> str | None:
|
|
28
|
+
return (
|
|
29
|
+
getattr(step, "out_rel", None)
|
|
30
|
+
or getattr(step, "outfile_rel", None)
|
|
31
|
+
or getattr(step, "outfile", None)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def eval_shell(ctx: Any, step: ShellStep) -> PlanItem:
|
|
36
|
+
if step.require_cmd and not ctx.have(step.require_cmd):
|
|
37
|
+
return PlanItem(
|
|
38
|
+
step.name, "SKIP", _out(step), f"missing tool: {step.require_cmd}"
|
|
39
|
+
)
|
|
40
|
+
return PlanItem(step.name, "RUN", _out(step), "")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def eval_ruff(ctx: Any, step: Any) -> PlanItem:
|
|
44
|
+
if ctx.options.no_ruff:
|
|
45
|
+
return PlanItem(step.name, "SKIP", _out(step), "disabled by --no-ruff")
|
|
46
|
+
if not ctx.have("ruff"):
|
|
47
|
+
return PlanItem(step.name, "SKIP", _out(step), "missing tool: ruff")
|
|
48
|
+
return PlanItem(step.name, "RUN", _out(step), "")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def eval_mypy(ctx: Any, step: MypyStep) -> PlanItem:
|
|
52
|
+
if ctx.options.no_mypy:
|
|
53
|
+
return PlanItem(step.name, "SKIP", _out(step), "disabled by --no-mypy")
|
|
54
|
+
if not ctx.have("mypy"):
|
|
55
|
+
return PlanItem(step.name, "SKIP", _out(step), "missing tool: mypy")
|
|
56
|
+
return PlanItem(step.name, "RUN", _out(step), "")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def eval_pytest(ctx: Any, step: PytestStep) -> PlanItem:
|
|
60
|
+
if ctx.options.no_pytest:
|
|
61
|
+
return PlanItem(step.name, "SKIP", _out(step), "disabled by --no-pytest")
|
|
62
|
+
if not ctx.have("pytest"):
|
|
63
|
+
return PlanItem(step.name, "SKIP", _out(step), "missing tool: pytest")
|
|
64
|
+
if (
|
|
65
|
+
not (ctx.root / "tests").exists()
|
|
66
|
+
and not (ctx.root / "sentra" / "tests").exists()
|
|
67
|
+
):
|
|
68
|
+
return PlanItem(step.name, "SKIP", _out(step), "no tests/ directory found")
|
|
69
|
+
return PlanItem(step.name, "RUN", _out(step), "")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def eval_rg(ctx: Any, step: Any) -> PlanItem:
|
|
73
|
+
if ctx.options.no_rg:
|
|
74
|
+
return PlanItem(step.name, "SKIP", _out(step), "disabled by --no-rg")
|
|
75
|
+
if not ctx.have("rg"):
|
|
76
|
+
return PlanItem(step.name, "SKIP", _out(step), "missing tool: rg")
|
|
77
|
+
return PlanItem(step.name, "RUN", _out(step), "")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
REGISTRY: list[tuple[type[Any], Callable[[Any, Any], PlanItem]]] = [
|
|
81
|
+
(ShellStep, eval_shell),
|
|
82
|
+
(RuffCheckStep, eval_ruff),
|
|
83
|
+
(RuffFormatCheckStep, eval_ruff),
|
|
84
|
+
(MypyStep, eval_mypy),
|
|
85
|
+
(PytestStep, eval_pytest),
|
|
86
|
+
(RipgrepScanStep, eval_rg),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def plan_for_profile(ctx: Any, profile: Any) -> list[PlanItem]:
|
|
91
|
+
items: list[PlanItem] = []
|
|
92
|
+
for step in profile.steps:
|
|
93
|
+
item: PlanItem | None = None
|
|
94
|
+
|
|
95
|
+
for cls, fn in REGISTRY:
|
|
96
|
+
if isinstance(step, cls):
|
|
97
|
+
item = fn(ctx, step)
|
|
98
|
+
break
|
|
99
|
+
if item is None:
|
|
100
|
+
item = PlanItem(step.name, "RUN", _out(step), "")
|
|
101
|
+
items.append(item)
|
|
102
|
+
return items
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def print_tool_info(ctx: Any) -> None:
|
|
106
|
+
"""Print tool availability with security validation status."""
|
|
107
|
+
|
|
108
|
+
tools_data = [
|
|
109
|
+
("git", ctx.tools.git),
|
|
110
|
+
("python", ctx.tools.python),
|
|
111
|
+
("pip", ctx.tools.pip),
|
|
112
|
+
("zip", ctx.tools.zip),
|
|
113
|
+
("tar", ctx.tools.tar),
|
|
114
|
+
("uname", ctx.tools.uname),
|
|
115
|
+
("ruff", ctx.tools.ruff),
|
|
116
|
+
("mypy", ctx.tools.mypy),
|
|
117
|
+
("pytest", ctx.tools.pytest),
|
|
118
|
+
("rg", ctx.tools.rg),
|
|
119
|
+
("tree", ctx.tools.tree),
|
|
120
|
+
("npm", ctx.tools.npm),
|
|
121
|
+
# Code quality tools (v1.3.0)
|
|
122
|
+
("vulture", ctx.tools.vulture),
|
|
123
|
+
("radon", ctx.tools.radon),
|
|
124
|
+
("interrogate", ctx.tools.interrogate),
|
|
125
|
+
("pylint", ctx.tools.pylint),
|
|
126
|
+
# Dependency analysis tools (v1.3.1)
|
|
127
|
+
("pipdeptree", ctx.tools.pipdeptree),
|
|
128
|
+
("pip-licenses", ctx.tools.pip_licenses),
|
|
129
|
+
# Performance profiling tools (v1.4.0)
|
|
130
|
+
("line_profiler", ctx.tools.line_profiler),
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
print("\n🔧 Tool Detection:")
|
|
134
|
+
print("=" * 70)
|
|
135
|
+
|
|
136
|
+
for name, path in tools_data:
|
|
137
|
+
if path:
|
|
138
|
+
trusted = is_path_trusted(path)
|
|
139
|
+
trust_marker = "✅" if trusted else "⚠️ "
|
|
140
|
+
print(f" {name:10} {trust_marker} {path}")
|
|
141
|
+
else:
|
|
142
|
+
print(f" {name:10} ❌ <missing>")
|
|
143
|
+
|
|
144
|
+
if ctx.options.strict_paths:
|
|
145
|
+
print("\n⚠️ STRICT-PATHS MODE ENABLED")
|
|
146
|
+
print(" Only tools in trusted directories are available.")
|
|
147
|
+
|
|
148
|
+
print("=" * 70)
|
pybundle/filters.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from fnmatch import fnmatch
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
EXCLUDE_PATTERNS = {
|
|
5
|
+
"*.egg",
|
|
6
|
+
"*.egg-info",
|
|
7
|
+
"*.appimage",
|
|
8
|
+
"*.deb",
|
|
9
|
+
"*.rpm",
|
|
10
|
+
"*.exe",
|
|
11
|
+
"*.msi",
|
|
12
|
+
"*.dmg",
|
|
13
|
+
"*.pkg",
|
|
14
|
+
"*.so",
|
|
15
|
+
"*.dll",
|
|
16
|
+
"*.dylib",
|
|
17
|
+
"*.db",
|
|
18
|
+
"*.sqlite",
|
|
19
|
+
"*.sqlite3",
|
|
20
|
+
"*.zip",
|
|
21
|
+
"*.tar",
|
|
22
|
+
"*.gz",
|
|
23
|
+
"*.tgz",
|
|
24
|
+
"*.bz2",
|
|
25
|
+
"*.xz",
|
|
26
|
+
"*.7z",
|
|
27
|
+
"*.rej",
|
|
28
|
+
"*.orig",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
DEFAULT_EXCLUDE_DIRS = {
|
|
32
|
+
".git",
|
|
33
|
+
".venv",
|
|
34
|
+
".mypy_cache",
|
|
35
|
+
".ruff_cache",
|
|
36
|
+
".pytest_cache",
|
|
37
|
+
"__pycache__",
|
|
38
|
+
"node_modules",
|
|
39
|
+
"dist",
|
|
40
|
+
"build",
|
|
41
|
+
"target",
|
|
42
|
+
".next",
|
|
43
|
+
".nuxt",
|
|
44
|
+
"artifacts",
|
|
45
|
+
".cache",
|
|
46
|
+
".hg",
|
|
47
|
+
".svn",
|
|
48
|
+
"venv",
|
|
49
|
+
".direnv",
|
|
50
|
+
".pybundle-venv",
|
|
51
|
+
"binaries",
|
|
52
|
+
"out",
|
|
53
|
+
".svelte-kit",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
DEFAULT_INCLUDE_FILES = [
|
|
57
|
+
"pyproject.toml",
|
|
58
|
+
"requirements.txt",
|
|
59
|
+
"poetry.lock",
|
|
60
|
+
"pdm.lock",
|
|
61
|
+
"uv.lock",
|
|
62
|
+
"setup.cfg",
|
|
63
|
+
"setup.py",
|
|
64
|
+
"mypy.ini",
|
|
65
|
+
"ruff.toml",
|
|
66
|
+
".ruff.toml",
|
|
67
|
+
"pytest.ini",
|
|
68
|
+
"tox.ini",
|
|
69
|
+
".python-version",
|
|
70
|
+
"README.md",
|
|
71
|
+
"README.rst",
|
|
72
|
+
"README.txt",
|
|
73
|
+
"CHANGELOG.md",
|
|
74
|
+
"LICENSE",
|
|
75
|
+
"LICENSE.md",
|
|
76
|
+
".tox",
|
|
77
|
+
".nox",
|
|
78
|
+
".direnv",
|
|
79
|
+
"requirements-dev.txt",
|
|
80
|
+
"package.json",
|
|
81
|
+
"package-lock.json",
|
|
82
|
+
"pnpm-lock.yaml",
|
|
83
|
+
"yarn.lock",
|
|
84
|
+
"tsconfig.json",
|
|
85
|
+
"vite.config.js",
|
|
86
|
+
"vite.config.ts",
|
|
87
|
+
"webpack.config.js",
|
|
88
|
+
"webpack.config.ts",
|
|
89
|
+
"Cargo.toml",
|
|
90
|
+
"Cargo.lock",
|
|
91
|
+
"tauri.conf.json",
|
|
92
|
+
"tauri.conf.json5",
|
|
93
|
+
"tauri.conf.toml",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
DEFAULT_INCLUDE_DIRS = [
|
|
97
|
+
"src",
|
|
98
|
+
"tests",
|
|
99
|
+
"tools",
|
|
100
|
+
"docs",
|
|
101
|
+
".github",
|
|
102
|
+
"app",
|
|
103
|
+
"templates",
|
|
104
|
+
"static",
|
|
105
|
+
"src-tauri",
|
|
106
|
+
"frontend",
|
|
107
|
+
"web",
|
|
108
|
+
"ui",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
DEFAULT_INCLUDE_GLOBS = [
|
|
112
|
+
"*.py",
|
|
113
|
+
"*/**/*.py",
|
|
114
|
+
"templates/**/*",
|
|
115
|
+
"static/**/*",
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
DEFAULT_EXCLUDE_FILE_EXTS: set[str] = {
|
|
119
|
+
".appimage",
|
|
120
|
+
".deb",
|
|
121
|
+
".rpm",
|
|
122
|
+
".exe",
|
|
123
|
+
".msi",
|
|
124
|
+
".dmg",
|
|
125
|
+
".pkg",
|
|
126
|
+
".so",
|
|
127
|
+
".dll",
|
|
128
|
+
".dylib",
|
|
129
|
+
".db",
|
|
130
|
+
".sqlite",
|
|
131
|
+
".sqlite3",
|
|
132
|
+
".zip",
|
|
133
|
+
".tar",
|
|
134
|
+
".gz",
|
|
135
|
+
".tgz",
|
|
136
|
+
".bz2",
|
|
137
|
+
".xz",
|
|
138
|
+
".7z",
|
|
139
|
+
".git",
|
|
140
|
+
".venv",
|
|
141
|
+
".mypy_cache",
|
|
142
|
+
".ruff_cache",
|
|
143
|
+
".pytest_cache",
|
|
144
|
+
"__pycache__",
|
|
145
|
+
"node_modules",
|
|
146
|
+
"dist",
|
|
147
|
+
"build",
|
|
148
|
+
"artifacts",
|
|
149
|
+
".cache",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def is_excluded_by_name(
|
|
154
|
+
name: str, *, exclude_names: set[str], exclude_patterns: set[str]
|
|
155
|
+
) -> bool:
|
|
156
|
+
if name in exclude_names:
|
|
157
|
+
return True
|
|
158
|
+
return any(fnmatch(name, pat) for pat in exclude_patterns)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_excluded_name(self, name: str) -> bool:
|
|
162
|
+
return is_excluded_by_name(
|
|
163
|
+
name, exclude_names=self.exclude_dirs, exclude_patterns=self.exclude_patterns
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def is_excluded_path(
|
|
168
|
+
rel: Path,
|
|
169
|
+
exclude_names: set[str],
|
|
170
|
+
exclude_patterns: set[str],
|
|
171
|
+
) -> bool:
|
|
172
|
+
# Exclude if *any* part matches (dirs) OR the final filename matches
|
|
173
|
+
for part in rel.parts:
|
|
174
|
+
if is_excluded_by_name(
|
|
175
|
+
part, exclude_names=exclude_names, exclude_patterns=exclude_patterns
|
|
176
|
+
):
|
|
177
|
+
return True
|
|
178
|
+
return False
|