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
pybundle/filters.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
"env", # Added
|
|
50
|
+
".direnv",
|
|
51
|
+
".pybundle-venv",
|
|
52
|
+
".freeze-venv", # Added - common in enterprise freeze workflows
|
|
53
|
+
"site-packages", # Added - never scan installed packages
|
|
54
|
+
"binaries",
|
|
55
|
+
"out",
|
|
56
|
+
".svelte-kit",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
DEFAULT_INCLUDE_FILES = [
|
|
60
|
+
"pyproject.toml",
|
|
61
|
+
"requirements.txt",
|
|
62
|
+
"poetry.lock",
|
|
63
|
+
"pdm.lock",
|
|
64
|
+
"uv.lock",
|
|
65
|
+
"setup.cfg",
|
|
66
|
+
"setup.py",
|
|
67
|
+
"mypy.ini",
|
|
68
|
+
"ruff.toml",
|
|
69
|
+
".ruff.toml",
|
|
70
|
+
"pytest.ini",
|
|
71
|
+
"tox.ini",
|
|
72
|
+
".python-version",
|
|
73
|
+
"README.md",
|
|
74
|
+
"README.rst",
|
|
75
|
+
"README.txt",
|
|
76
|
+
"CHANGELOG.md",
|
|
77
|
+
"LICENSE",
|
|
78
|
+
"LICENSE.md",
|
|
79
|
+
".tox",
|
|
80
|
+
".nox",
|
|
81
|
+
".direnv",
|
|
82
|
+
"requirements-dev.txt",
|
|
83
|
+
"package.json",
|
|
84
|
+
"package-lock.json",
|
|
85
|
+
"pnpm-lock.yaml",
|
|
86
|
+
"yarn.lock",
|
|
87
|
+
"tsconfig.json",
|
|
88
|
+
"vite.config.js",
|
|
89
|
+
"vite.config.ts",
|
|
90
|
+
"webpack.config.js",
|
|
91
|
+
"webpack.config.ts",
|
|
92
|
+
"Cargo.toml",
|
|
93
|
+
"Cargo.lock",
|
|
94
|
+
"tauri.conf.json",
|
|
95
|
+
"tauri.conf.json5",
|
|
96
|
+
"tauri.conf.toml",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
DEFAULT_INCLUDE_DIRS = [
|
|
100
|
+
"src",
|
|
101
|
+
"tests",
|
|
102
|
+
"tools",
|
|
103
|
+
"docs",
|
|
104
|
+
".github",
|
|
105
|
+
"app",
|
|
106
|
+
"templates",
|
|
107
|
+
"static",
|
|
108
|
+
"src-tauri",
|
|
109
|
+
"frontend",
|
|
110
|
+
"web",
|
|
111
|
+
"ui",
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
DEFAULT_INCLUDE_GLOBS = [
|
|
115
|
+
"*.py",
|
|
116
|
+
"*/**/*.py",
|
|
117
|
+
"templates/**/*",
|
|
118
|
+
"static/**/*",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
DEFAULT_EXCLUDE_FILE_EXTS: set[str] = {
|
|
122
|
+
".appimage",
|
|
123
|
+
".deb",
|
|
124
|
+
".rpm",
|
|
125
|
+
".exe",
|
|
126
|
+
".msi",
|
|
127
|
+
".dmg",
|
|
128
|
+
".pkg",
|
|
129
|
+
".so",
|
|
130
|
+
".dll",
|
|
131
|
+
".dylib",
|
|
132
|
+
".db",
|
|
133
|
+
".sqlite",
|
|
134
|
+
".sqlite3",
|
|
135
|
+
".zip",
|
|
136
|
+
".tar",
|
|
137
|
+
".gz",
|
|
138
|
+
".tgz",
|
|
139
|
+
".bz2",
|
|
140
|
+
".xz",
|
|
141
|
+
".7z",
|
|
142
|
+
".git",
|
|
143
|
+
".venv",
|
|
144
|
+
".mypy_cache",
|
|
145
|
+
".ruff_cache",
|
|
146
|
+
".pytest_cache",
|
|
147
|
+
"__pycache__",
|
|
148
|
+
"node_modules",
|
|
149
|
+
"dist",
|
|
150
|
+
"build",
|
|
151
|
+
"artifacts",
|
|
152
|
+
".cache",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def is_excluded_by_name(
|
|
157
|
+
name: str, *, exclude_names: set[str], exclude_patterns: set[str]
|
|
158
|
+
) -> bool:
|
|
159
|
+
if name in exclude_names:
|
|
160
|
+
return True
|
|
161
|
+
return any(fnmatch(name, pat) for pat in exclude_patterns)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def is_excluded_name(self, name: str) -> bool:
|
|
165
|
+
return is_excluded_by_name(
|
|
166
|
+
name, exclude_names=self.exclude_dirs, exclude_patterns=self.exclude_patterns
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def is_excluded_path(
|
|
171
|
+
rel: Path,
|
|
172
|
+
exclude_names: set[str],
|
|
173
|
+
exclude_patterns: set[str],
|
|
174
|
+
) -> bool:
|
|
175
|
+
# Exclude if *any* part matches (dirs) OR the final filename matches
|
|
176
|
+
for part in rel.parts:
|
|
177
|
+
if is_excluded_by_name(
|
|
178
|
+
part, exclude_names=exclude_names, exclude_patterns=exclude_patterns
|
|
179
|
+
):
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def should_exclude_from_analysis(path: Path) -> bool:
|
|
185
|
+
"""Check if path should be excluded from project analysis.
|
|
186
|
+
|
|
187
|
+
This is a comprehensive filter for ALL analysis steps to prevent
|
|
188
|
+
scanning dependencies, caches, and build artifacts.
|
|
189
|
+
|
|
190
|
+
Use this for: type coverage, docstring coverage, link validation,
|
|
191
|
+
complexity analysis, and any other metrics that should reflect
|
|
192
|
+
PROJECT code only, not dependencies.
|
|
193
|
+
"""
|
|
194
|
+
parts = set(path.parts)
|
|
195
|
+
|
|
196
|
+
# Comprehensive list of dependency/cache/build directories
|
|
197
|
+
exclude_patterns = {
|
|
198
|
+
# Virtual environments (all common patterns)
|
|
199
|
+
".venv", "venv", "env", ".env",
|
|
200
|
+
".pybundle-venv", ".freeze-venv",
|
|
201
|
+
# Match any venv-like directory (e.g., .gaslog-venv)
|
|
202
|
+
# Check if any part ends with -venv or starts with venv
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Additional checks for venv-like patterns
|
|
206
|
+
for part in parts:
|
|
207
|
+
if part.endswith("-venv") or part.endswith("_venv"):
|
|
208
|
+
return True
|
|
209
|
+
if "site-packages" in part:
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
# Standard exclusions
|
|
213
|
+
if parts & {
|
|
214
|
+
# Caches
|
|
215
|
+
".mypy_cache", ".ruff_cache", ".pytest_cache", "__pycache__",
|
|
216
|
+
".cache", ".tox", ".nox",
|
|
217
|
+
# Build outputs
|
|
218
|
+
"build", "dist", "target", "out", "artifacts",
|
|
219
|
+
# Version control
|
|
220
|
+
".git", ".hg", ".svn",
|
|
221
|
+
# Dependencies
|
|
222
|
+
"node_modules",
|
|
223
|
+
# Other
|
|
224
|
+
"binaries",
|
|
225
|
+
}:
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
return False
|
pybundle/manifest.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess # nosec B404 - Required for git operations, paths validated
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .context import BundleContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _git_commit_hash(root: Path, git_path: str | None) -> str | None:
|
|
13
|
+
"""Return HEAD commit hash if root is inside a git repo, else None."""
|
|
14
|
+
if not git_path:
|
|
15
|
+
return None
|
|
16
|
+
try:
|
|
17
|
+
# If this fails, we're not in a repo or git isn't functional.
|
|
18
|
+
p = subprocess.run( # nosec B603
|
|
19
|
+
[git_path, "rev-parse", "HEAD"],
|
|
20
|
+
cwd=str(root),
|
|
21
|
+
capture_output=True,
|
|
22
|
+
text=True,
|
|
23
|
+
check=True,
|
|
24
|
+
)
|
|
25
|
+
return p.stdout.strip() or None
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_manifest(
|
|
31
|
+
*,
|
|
32
|
+
ctx: BundleContext,
|
|
33
|
+
profile_name: str,
|
|
34
|
+
archive_path: Path,
|
|
35
|
+
archive_format_used: str,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Write a stable, machine-readable manifest for automation."""
|
|
38
|
+
git_hash = _git_commit_hash(ctx.root, ctx.tools.git)
|
|
39
|
+
|
|
40
|
+
manifest: dict[str, Any] = {
|
|
41
|
+
"schema_version": 1,
|
|
42
|
+
"tool": {"name": "pybundle"},
|
|
43
|
+
"timestamp_utc": ctx.ts,
|
|
44
|
+
"profile": profile_name,
|
|
45
|
+
"paths": {
|
|
46
|
+
"root": str(ctx.root),
|
|
47
|
+
"workdir": str(ctx.workdir),
|
|
48
|
+
"srcdir": str(ctx.srcdir),
|
|
49
|
+
"logdir": str(ctx.logdir),
|
|
50
|
+
"metadir": str(ctx.metadir),
|
|
51
|
+
},
|
|
52
|
+
"outputs": {
|
|
53
|
+
"archive": {
|
|
54
|
+
"path": str(archive_path),
|
|
55
|
+
"name": archive_path.name,
|
|
56
|
+
"format": archive_format_used,
|
|
57
|
+
},
|
|
58
|
+
"summary_json": str(ctx.summary_json),
|
|
59
|
+
"manifest_json": str(ctx.manifest_json),
|
|
60
|
+
"runlog": str(ctx.runlog),
|
|
61
|
+
},
|
|
62
|
+
"options": asdict(ctx.options),
|
|
63
|
+
"run": {
|
|
64
|
+
"strict": ctx.strict,
|
|
65
|
+
"redact": ctx.redact,
|
|
66
|
+
"keep_workdir": ctx.keep_workdir,
|
|
67
|
+
"archive_format_requested": ctx.archive_format,
|
|
68
|
+
"name_prefix": ctx.name_prefix,
|
|
69
|
+
},
|
|
70
|
+
"tools": asdict(ctx.tools),
|
|
71
|
+
"git": {"commit": git_hash},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ctx.manifest_json.write_text(
|
|
75
|
+
json.dumps(manifest, indent=2, sort_keys=True),
|
|
76
|
+
encoding="utf-8",
|
|
77
|
+
)
|
pybundle/packaging.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess # nosec B404 - Required for archive creation, paths validated
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .context import BundleContext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_archive_format(ctx: BundleContext) -> str:
|
|
10
|
+
fmt_used = ctx.archive_format
|
|
11
|
+
if fmt_used == "auto":
|
|
12
|
+
fmt_used = "zip" if ctx.tools.zip else "tar.gz"
|
|
13
|
+
return fmt_used
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def archive_output_path(ctx: BundleContext, fmt_used: str) -> Path:
|
|
17
|
+
if fmt_used == "zip":
|
|
18
|
+
return ctx.outdir / f"{ctx.name_prefix}.zip"
|
|
19
|
+
return ctx.outdir / f"{ctx.name_prefix}.tar.gz"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_archive(ctx: BundleContext) -> tuple[Path, str]:
|
|
23
|
+
fmt_used = resolve_archive_format(ctx)
|
|
24
|
+
|
|
25
|
+
if fmt_used == "zip":
|
|
26
|
+
if not ctx.tools.zip:
|
|
27
|
+
raise RuntimeError("zip tool not available")
|
|
28
|
+
out = archive_output_path(ctx, fmt_used)
|
|
29
|
+
# zip wants working dir above target folder
|
|
30
|
+
subprocess.run( # nosec B603
|
|
31
|
+
[ctx.tools.zip, "-qr", str(out), ctx.workdir.name],
|
|
32
|
+
cwd=str(ctx.workdir.parent),
|
|
33
|
+
check=False,
|
|
34
|
+
)
|
|
35
|
+
return out, fmt_used
|
|
36
|
+
|
|
37
|
+
if not ctx.tools.tar:
|
|
38
|
+
raise RuntimeError("tar tool not available")
|
|
39
|
+
out = archive_output_path(ctx, fmt_used)
|
|
40
|
+
subprocess.run( # nosec B603
|
|
41
|
+
[ctx.tools.tar, "-czf", str(out), ctx.workdir.name],
|
|
42
|
+
cwd=str(ctx.workdir.parent),
|
|
43
|
+
check=False,
|
|
44
|
+
)
|
|
45
|
+
return out, fmt_used
|
pybundle/policy.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# pybundle/policy.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pybundle.filters import (
|
|
8
|
+
DEFAULT_EXCLUDE_DIRS,
|
|
9
|
+
DEFAULT_EXCLUDE_FILE_EXTS,
|
|
10
|
+
DEFAULT_INCLUDE_FILES,
|
|
11
|
+
DEFAULT_INCLUDE_DIRS,
|
|
12
|
+
DEFAULT_INCLUDE_GLOBS,
|
|
13
|
+
EXCLUDE_PATTERNS,
|
|
14
|
+
is_excluded_by_name,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class AIContextPolicy:
|
|
20
|
+
exclude_dirs: set[str] = field(default_factory=lambda: set(DEFAULT_EXCLUDE_DIRS))
|
|
21
|
+
exclude_patterns: set[str] = field(default_factory=lambda: set(EXCLUDE_PATTERNS))
|
|
22
|
+
exclude_file_exts: set[str] = field(
|
|
23
|
+
default_factory=lambda: set(DEFAULT_EXCLUDE_FILE_EXTS)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
include_files: list[str] = field(
|
|
27
|
+
default_factory=lambda: list(DEFAULT_INCLUDE_FILES)
|
|
28
|
+
)
|
|
29
|
+
include_dirs: list[str] = field(default_factory=lambda: list(DEFAULT_INCLUDE_DIRS))
|
|
30
|
+
include_globs: list[str] = field(
|
|
31
|
+
default_factory=lambda: list(DEFAULT_INCLUDE_GLOBS)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# AI-friendly knobs
|
|
35
|
+
tree_max_depth: int = 4
|
|
36
|
+
largest_limit: int = 80
|
|
37
|
+
roadmap_max_files: int = 20000
|
|
38
|
+
roadmap_mermaid_depth: int = 2
|
|
39
|
+
roadmap_mermaid_max_edges: int = 180
|
|
40
|
+
|
|
41
|
+
def include_dir_candidates(self, root: Path) -> list[Path]:
|
|
42
|
+
out: list[Path] = []
|
|
43
|
+
for d in self.include_dirs:
|
|
44
|
+
p = root / d
|
|
45
|
+
if p.exists():
|
|
46
|
+
out.append(p)
|
|
47
|
+
return out or [root]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class PathFilter:
|
|
52
|
+
"""
|
|
53
|
+
Shared filtering logic across steps:
|
|
54
|
+
- prune excluded dir names
|
|
55
|
+
- prune venvs by structure (any name)
|
|
56
|
+
- optionally exclude noisy file types by extension
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
exclude_dirs: set[str]
|
|
60
|
+
exclude_file_exts: set[str]
|
|
61
|
+
exclude_patterns: set[str] = field(default_factory=set)
|
|
62
|
+
detect_venvs: bool = True
|
|
63
|
+
|
|
64
|
+
def is_venv_root(self, p: Path) -> bool:
|
|
65
|
+
if not p.is_dir():
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
if (p / "pyvenv.cfg").is_file():
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
if (p / "bin").is_dir():
|
|
72
|
+
if (p / "bin" / "activate").is_file() and (
|
|
73
|
+
(p / "bin" / "python").exists() or (p / "bin" / "python3").exists()
|
|
74
|
+
):
|
|
75
|
+
return True
|
|
76
|
+
if any((p / "lib").glob("python*/site-packages")):
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
if (p / "Scripts").is_dir():
|
|
80
|
+
if (p / "Scripts" / "activate").is_file() and (
|
|
81
|
+
(p / "Scripts" / "python.exe").is_file()
|
|
82
|
+
or (p / "Scripts" / "python").exists()
|
|
83
|
+
):
|
|
84
|
+
return True
|
|
85
|
+
if (p / "Lib" / "site-packages").is_dir():
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
if (p / ".Python").exists():
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def should_prune_dir(self, parent_dir: Path, child_name: str) -> bool:
|
|
94
|
+
if is_excluded_by_name(
|
|
95
|
+
child_name,
|
|
96
|
+
exclude_names=self.exclude_dirs,
|
|
97
|
+
exclude_patterns=self.exclude_patterns,
|
|
98
|
+
):
|
|
99
|
+
return True
|
|
100
|
+
if self.detect_venvs and self.is_venv_root(parent_dir / child_name):
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def should_include_file(self, root: Path, p: Path) -> bool:
|
|
105
|
+
try:
|
|
106
|
+
rel = p.relative_to(root)
|
|
107
|
+
except Exception:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
# reject files under excluded dirs by name/pattern
|
|
111
|
+
for part in rel.parts[:-1]:
|
|
112
|
+
if is_excluded_by_name(
|
|
113
|
+
part,
|
|
114
|
+
exclude_names=self.exclude_dirs,
|
|
115
|
+
exclude_patterns=self.exclude_patterns,
|
|
116
|
+
):
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# reject excluded file names by pattern (e.g. *.egg, *.rej)
|
|
120
|
+
if is_excluded_by_name(
|
|
121
|
+
rel.name,
|
|
122
|
+
exclude_names=self.exclude_dirs,
|
|
123
|
+
exclude_patterns=self.exclude_patterns,
|
|
124
|
+
):
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
# reject excluded extensions
|
|
128
|
+
ext = p.suffix.lower()
|
|
129
|
+
if ext in self.exclude_file_exts:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
return True
|