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
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