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
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import subprocess # nosec B404 - Required for tool execution, paths validated
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .base import StepResult
|
|
10
|
+
from ..context import BundleContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _find_python_files(root: Path, max_files: int = 100) -> list[Path]:
|
|
14
|
+
"""Find Python files in the project, excluding common ignore directories."""
|
|
15
|
+
files = []
|
|
16
|
+
ignore_dirs = {
|
|
17
|
+
".venv",
|
|
18
|
+
"venv",
|
|
19
|
+
"__pycache__",
|
|
20
|
+
".mypy_cache",
|
|
21
|
+
".ruff_cache",
|
|
22
|
+
".pytest_cache",
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist",
|
|
25
|
+
"build",
|
|
26
|
+
"artifacts",
|
|
27
|
+
".git",
|
|
28
|
+
".tox",
|
|
29
|
+
"site-packages",
|
|
30
|
+
".eggs",
|
|
31
|
+
"*.egg-info",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for py_file in root.rglob("*.py"):
|
|
35
|
+
# Skip if any parent directory is in ignore list or matches pattern
|
|
36
|
+
should_skip = False
|
|
37
|
+
for part in py_file.parts:
|
|
38
|
+
# Check exact matches
|
|
39
|
+
if part in ignore_dirs:
|
|
40
|
+
should_skip = True
|
|
41
|
+
break
|
|
42
|
+
# Check pattern matches (e.g., .pybundle-venv, any .venv variant)
|
|
43
|
+
if part.startswith(".") and "venv" in part:
|
|
44
|
+
should_skip = True
|
|
45
|
+
break
|
|
46
|
+
if part.endswith(".egg-info") or part.endswith("-info"):
|
|
47
|
+
should_skip = True
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
if should_skip:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
files.append(py_file)
|
|
54
|
+
if len(files) >= max_files:
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
return files
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _check_syntax_errors(file_path: Path) -> list[dict]:
|
|
61
|
+
"""Check a Python file for syntax errors."""
|
|
62
|
+
errors = []
|
|
63
|
+
try:
|
|
64
|
+
content = file_path.read_text(encoding="utf-8")
|
|
65
|
+
ast.parse(content, filename=str(file_path))
|
|
66
|
+
except SyntaxError as e:
|
|
67
|
+
errors.append(
|
|
68
|
+
{
|
|
69
|
+
"line": e.lineno or 0,
|
|
70
|
+
"offset": e.offset or 0,
|
|
71
|
+
"message": e.msg,
|
|
72
|
+
"text": e.text.strip() if e.text else "",
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
errors.append(
|
|
77
|
+
{
|
|
78
|
+
"line": 0,
|
|
79
|
+
"offset": 0,
|
|
80
|
+
"message": f"Error reading file: {str(e)}",
|
|
81
|
+
"text": "",
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return errors
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _find_imports(file_path: Path) -> tuple[set[str], list[dict]]:
|
|
89
|
+
"""Extract import statements from a Python file."""
|
|
90
|
+
imports = set()
|
|
91
|
+
import_errors = []
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
content = file_path.read_text(encoding="utf-8")
|
|
95
|
+
tree = ast.parse(content, filename=str(file_path))
|
|
96
|
+
|
|
97
|
+
for node in ast.walk(tree):
|
|
98
|
+
if isinstance(node, ast.Import):
|
|
99
|
+
for alias in node.names:
|
|
100
|
+
imports.add(alias.name.split(".")[0])
|
|
101
|
+
elif isinstance(node, ast.ImportFrom):
|
|
102
|
+
# Skip relative imports (level > 0, e.g., "from . import x", "from .steps import y")
|
|
103
|
+
if node.level > 0:
|
|
104
|
+
continue
|
|
105
|
+
# For "from X import Y", we care about the module X
|
|
106
|
+
if node.module:
|
|
107
|
+
imports.add(node.module.split(".")[0])
|
|
108
|
+
except Exception as e:
|
|
109
|
+
import_errors.append({"file": str(file_path), "error": str(e)})
|
|
110
|
+
|
|
111
|
+
return imports, import_errors
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _get_project_modules(root: Path) -> set[str]:
|
|
115
|
+
"""Get module names that are part of the project itself."""
|
|
116
|
+
modules = set()
|
|
117
|
+
|
|
118
|
+
# Look for Python packages (directories with __init__.py) at root level
|
|
119
|
+
for item in root.iterdir():
|
|
120
|
+
if (
|
|
121
|
+
item.is_dir()
|
|
122
|
+
and not item.name.startswith(".")
|
|
123
|
+
and not item.name.startswith("_")
|
|
124
|
+
):
|
|
125
|
+
# Check if it's a Python package
|
|
126
|
+
if (item / "__init__.py").exists():
|
|
127
|
+
modules.add(item.name)
|
|
128
|
+
|
|
129
|
+
# Also add all .py files within this package as potential modules
|
|
130
|
+
# (they can be imported as package.module)
|
|
131
|
+
for py_file in item.rglob("*.py"):
|
|
132
|
+
if py_file.name != "__init__.py":
|
|
133
|
+
# Get the module name relative to root
|
|
134
|
+
# e.g., pybundle/cli.py -> add 'cli' as a project module
|
|
135
|
+
modules.add(py_file.stem)
|
|
136
|
+
|
|
137
|
+
# Also add Python files in root (without .py extension)
|
|
138
|
+
for item in root.glob("*.py"):
|
|
139
|
+
if item.name != "__init__.py":
|
|
140
|
+
modules.add(item.stem)
|
|
141
|
+
|
|
142
|
+
return modules
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _get_stdlib_modules() -> set[str]:
|
|
146
|
+
"""Get set of standard library module names."""
|
|
147
|
+
try:
|
|
148
|
+
import sys
|
|
149
|
+
|
|
150
|
+
# Python 3.10+ has this
|
|
151
|
+
if hasattr(sys, "stdlib_module_names"):
|
|
152
|
+
return set(sys.stdlib_module_names)
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
# Fallback for older Python versions or if the above fails
|
|
157
|
+
# This is a comprehensive list for Python 3.8+
|
|
158
|
+
return {
|
|
159
|
+
"__future__",
|
|
160
|
+
"abc",
|
|
161
|
+
"aifc",
|
|
162
|
+
"argparse",
|
|
163
|
+
"array",
|
|
164
|
+
"ast",
|
|
165
|
+
"asynchat",
|
|
166
|
+
"asyncio",
|
|
167
|
+
"asyncore",
|
|
168
|
+
"atexit",
|
|
169
|
+
"audioop",
|
|
170
|
+
"base64",
|
|
171
|
+
"bdb",
|
|
172
|
+
"binascii",
|
|
173
|
+
"bisect",
|
|
174
|
+
"builtins",
|
|
175
|
+
"bz2",
|
|
176
|
+
"calendar",
|
|
177
|
+
"cgi",
|
|
178
|
+
"cgitb",
|
|
179
|
+
"chunk",
|
|
180
|
+
"cmath",
|
|
181
|
+
"cmd",
|
|
182
|
+
"code",
|
|
183
|
+
"codecs",
|
|
184
|
+
"codeop",
|
|
185
|
+
"collections",
|
|
186
|
+
"colorsys",
|
|
187
|
+
"compileall",
|
|
188
|
+
"concurrent",
|
|
189
|
+
"configparser",
|
|
190
|
+
"contextlib",
|
|
191
|
+
"contextvars",
|
|
192
|
+
"copy",
|
|
193
|
+
"copyreg",
|
|
194
|
+
"cProfile",
|
|
195
|
+
"crypt",
|
|
196
|
+
"csv",
|
|
197
|
+
"ctypes",
|
|
198
|
+
"curses",
|
|
199
|
+
"dataclasses",
|
|
200
|
+
"datetime",
|
|
201
|
+
"dbm",
|
|
202
|
+
"decimal",
|
|
203
|
+
"difflib",
|
|
204
|
+
"dis",
|
|
205
|
+
"distutils",
|
|
206
|
+
"doctest",
|
|
207
|
+
"email",
|
|
208
|
+
"encodings",
|
|
209
|
+
"enum",
|
|
210
|
+
"errno",
|
|
211
|
+
"faulthandler",
|
|
212
|
+
"fcntl",
|
|
213
|
+
"filecmp",
|
|
214
|
+
"fileinput",
|
|
215
|
+
"fnmatch",
|
|
216
|
+
"fractions",
|
|
217
|
+
"ftplib",
|
|
218
|
+
"functools",
|
|
219
|
+
"gc",
|
|
220
|
+
"getopt",
|
|
221
|
+
"getpass",
|
|
222
|
+
"gettext",
|
|
223
|
+
"glob",
|
|
224
|
+
"graphlib",
|
|
225
|
+
"grp",
|
|
226
|
+
"gzip",
|
|
227
|
+
"hashlib",
|
|
228
|
+
"heapq",
|
|
229
|
+
"hmac",
|
|
230
|
+
"html",
|
|
231
|
+
"http",
|
|
232
|
+
"imaplib",
|
|
233
|
+
"imghdr",
|
|
234
|
+
"imp",
|
|
235
|
+
"importlib",
|
|
236
|
+
"inspect",
|
|
237
|
+
"io",
|
|
238
|
+
"ipaddress",
|
|
239
|
+
"itertools",
|
|
240
|
+
"json",
|
|
241
|
+
"keyword",
|
|
242
|
+
"lib2to3",
|
|
243
|
+
"linecache",
|
|
244
|
+
"locale",
|
|
245
|
+
"logging",
|
|
246
|
+
"lzma",
|
|
247
|
+
"mailbox",
|
|
248
|
+
"mailcap",
|
|
249
|
+
"marshal",
|
|
250
|
+
"math",
|
|
251
|
+
"mimetypes",
|
|
252
|
+
"mmap",
|
|
253
|
+
"modulefinder",
|
|
254
|
+
"multiprocessing",
|
|
255
|
+
"netrc",
|
|
256
|
+
"nis",
|
|
257
|
+
"nntplib",
|
|
258
|
+
"numbers",
|
|
259
|
+
"operator",
|
|
260
|
+
"optparse",
|
|
261
|
+
"os",
|
|
262
|
+
"ossaudiodev",
|
|
263
|
+
"pathlib",
|
|
264
|
+
"pdb",
|
|
265
|
+
"pickle",
|
|
266
|
+
"pickletools",
|
|
267
|
+
"pipes",
|
|
268
|
+
"pkgutil",
|
|
269
|
+
"platform",
|
|
270
|
+
"plistlib",
|
|
271
|
+
"poplib",
|
|
272
|
+
"posix",
|
|
273
|
+
"posixpath",
|
|
274
|
+
"pprint",
|
|
275
|
+
"profile",
|
|
276
|
+
"pstats",
|
|
277
|
+
"pty",
|
|
278
|
+
"pwd",
|
|
279
|
+
"py_compile",
|
|
280
|
+
"pyclbr",
|
|
281
|
+
"pydoc",
|
|
282
|
+
"queue",
|
|
283
|
+
"quopri",
|
|
284
|
+
"random",
|
|
285
|
+
"re",
|
|
286
|
+
"readline",
|
|
287
|
+
"reprlib",
|
|
288
|
+
"resource",
|
|
289
|
+
"rlcompleter",
|
|
290
|
+
"runpy",
|
|
291
|
+
"sched",
|
|
292
|
+
"secrets",
|
|
293
|
+
"select",
|
|
294
|
+
"selectors",
|
|
295
|
+
"shelve",
|
|
296
|
+
"shlex",
|
|
297
|
+
"shutil",
|
|
298
|
+
"signal",
|
|
299
|
+
"site",
|
|
300
|
+
"smtpd",
|
|
301
|
+
"smtplib",
|
|
302
|
+
"sndhdr",
|
|
303
|
+
"socket",
|
|
304
|
+
"socketserver",
|
|
305
|
+
"spwd",
|
|
306
|
+
"sqlite3",
|
|
307
|
+
"ssl",
|
|
308
|
+
"stat",
|
|
309
|
+
"statistics",
|
|
310
|
+
"string",
|
|
311
|
+
"stringprep",
|
|
312
|
+
"struct",
|
|
313
|
+
"subprocess",
|
|
314
|
+
"sunau",
|
|
315
|
+
"symbol",
|
|
316
|
+
"symtable",
|
|
317
|
+
"sys",
|
|
318
|
+
"sysconfig",
|
|
319
|
+
"syslog",
|
|
320
|
+
"tabnanny",
|
|
321
|
+
"tarfile",
|
|
322
|
+
"telnetlib",
|
|
323
|
+
"tempfile",
|
|
324
|
+
"termios",
|
|
325
|
+
"test",
|
|
326
|
+
"textwrap",
|
|
327
|
+
"threading",
|
|
328
|
+
"time",
|
|
329
|
+
"timeit",
|
|
330
|
+
"tkinter",
|
|
331
|
+
"token",
|
|
332
|
+
"tokenize",
|
|
333
|
+
"tomllib",
|
|
334
|
+
"trace",
|
|
335
|
+
"traceback",
|
|
336
|
+
"tracemalloc",
|
|
337
|
+
"tty",
|
|
338
|
+
"turtle",
|
|
339
|
+
"turtledemo",
|
|
340
|
+
"types",
|
|
341
|
+
"typing",
|
|
342
|
+
"typing_extensions",
|
|
343
|
+
"unicodedata",
|
|
344
|
+
"unittest",
|
|
345
|
+
"urllib",
|
|
346
|
+
"uu",
|
|
347
|
+
"uuid",
|
|
348
|
+
"venv",
|
|
349
|
+
"warnings",
|
|
350
|
+
"wave",
|
|
351
|
+
"weakref",
|
|
352
|
+
"webbrowser",
|
|
353
|
+
"winreg",
|
|
354
|
+
"winsound",
|
|
355
|
+
"wsgiref",
|
|
356
|
+
"xdrlib",
|
|
357
|
+
"xml",
|
|
358
|
+
"xmlrpc",
|
|
359
|
+
"zipapp",
|
|
360
|
+
"zipfile",
|
|
361
|
+
"zipimport",
|
|
362
|
+
"zlib",
|
|
363
|
+
"zoneinfo",
|
|
364
|
+
# Python 3.13 additions
|
|
365
|
+
"annotationlib",
|
|
366
|
+
"dbm.sqlite3",
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _can_import(module_name: str) -> bool:
|
|
371
|
+
"""Test if a module can be imported."""
|
|
372
|
+
try:
|
|
373
|
+
import importlib.util
|
|
374
|
+
|
|
375
|
+
spec = importlib.util.find_spec(module_name)
|
|
376
|
+
return spec is not None
|
|
377
|
+
except (ImportError, ModuleNotFoundError, ValueError, AttributeError):
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@dataclass
|
|
382
|
+
class PylanceStep:
|
|
383
|
+
name: str = "pylance"
|
|
384
|
+
outfile: str = "logs/34_pylance.txt"
|
|
385
|
+
max_files: int = 100
|
|
386
|
+
|
|
387
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
388
|
+
start = time.time()
|
|
389
|
+
out = ctx.workdir / self.outfile
|
|
390
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
391
|
+
|
|
392
|
+
sections = []
|
|
393
|
+
sections.append("## Pylance-Style Analysis Report ##\n")
|
|
394
|
+
sections.append(f"## PWD: {ctx.root}\n\n")
|
|
395
|
+
|
|
396
|
+
has_issues = False
|
|
397
|
+
|
|
398
|
+
# Find Python files
|
|
399
|
+
py_files = _find_python_files(ctx.root, self.max_files)
|
|
400
|
+
sections.append(f"Found {len(py_files)} Python files to analyze\n\n")
|
|
401
|
+
|
|
402
|
+
# 1. Syntax Errors
|
|
403
|
+
sections.append("## Syntax Errors ##\n")
|
|
404
|
+
syntax_error_count = 0
|
|
405
|
+
|
|
406
|
+
for py_file in py_files:
|
|
407
|
+
errors = _check_syntax_errors(py_file)
|
|
408
|
+
if errors:
|
|
409
|
+
has_issues = True
|
|
410
|
+
syntax_error_count += len(errors)
|
|
411
|
+
rel_path = py_file.relative_to(ctx.root)
|
|
412
|
+
sections.append(f"\n{rel_path}:\n")
|
|
413
|
+
for err in errors:
|
|
414
|
+
sections.append(
|
|
415
|
+
f" Line {err['line']}, Col {err['offset']}: {err['message']}\n"
|
|
416
|
+
)
|
|
417
|
+
if err["text"]:
|
|
418
|
+
sections.append(f" {err['text']}\n")
|
|
419
|
+
|
|
420
|
+
if syntax_error_count == 0:
|
|
421
|
+
sections.append("No syntax errors found.\n")
|
|
422
|
+
else:
|
|
423
|
+
sections.append(f"\nTotal syntax errors: {syntax_error_count}\n")
|
|
424
|
+
|
|
425
|
+
# 2. Import Analysis
|
|
426
|
+
sections.append("\n## Import Analysis ##\n")
|
|
427
|
+
all_imports = set()
|
|
428
|
+
import_errors = []
|
|
429
|
+
|
|
430
|
+
for py_file in py_files:
|
|
431
|
+
file_imports, file_errors = _find_imports(py_file)
|
|
432
|
+
all_imports.update(file_imports)
|
|
433
|
+
import_errors.extend(file_errors)
|
|
434
|
+
|
|
435
|
+
sections.append(f"Total unique top-level imports found: {len(all_imports)}\n")
|
|
436
|
+
|
|
437
|
+
# Check which imports might be missing
|
|
438
|
+
stdlib_modules = _get_stdlib_modules()
|
|
439
|
+
project_modules = _get_project_modules(ctx.root)
|
|
440
|
+
|
|
441
|
+
# Filter out private imports and check if modules can be imported
|
|
442
|
+
public_imports = {imp for imp in all_imports if not imp.startswith("_")}
|
|
443
|
+
|
|
444
|
+
potentially_missing = []
|
|
445
|
+
for imp in sorted(public_imports):
|
|
446
|
+
# Skip if it's in stdlib
|
|
447
|
+
if imp in stdlib_modules:
|
|
448
|
+
continue
|
|
449
|
+
|
|
450
|
+
# Skip if it's a local project module
|
|
451
|
+
if imp in project_modules:
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
# Skip common builtin names
|
|
455
|
+
if imp in {"StringIO", "BytesIO", "io"}: # io module variations
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
# Try to import it - if it fails, it's potentially missing
|
|
459
|
+
if not _can_import(imp):
|
|
460
|
+
potentially_missing.append(imp)
|
|
461
|
+
|
|
462
|
+
if potentially_missing:
|
|
463
|
+
has_issues = True
|
|
464
|
+
sections.append(
|
|
465
|
+
f"\nPotentially missing/unimportable modules ({len(potentially_missing)}):\n"
|
|
466
|
+
)
|
|
467
|
+
sections.append(
|
|
468
|
+
"(Could not be imported - either missing dependencies or sub-modules)\n"
|
|
469
|
+
)
|
|
470
|
+
sections.append(
|
|
471
|
+
"Note: Sub-modules of installed packages (e.g., 'requests.exceptions') may appear here.\n\n"
|
|
472
|
+
)
|
|
473
|
+
for imp in potentially_missing[:30]: # Limit to first 30
|
|
474
|
+
sections.append(f" - {imp}\n")
|
|
475
|
+
|
|
476
|
+
# Add helpful note about common cases
|
|
477
|
+
known_submodules = {
|
|
478
|
+
"connection",
|
|
479
|
+
"connectionpool",
|
|
480
|
+
"poolmanager",
|
|
481
|
+
"response",
|
|
482
|
+
"exceptions",
|
|
483
|
+
"contrib",
|
|
484
|
+
"fields",
|
|
485
|
+
"filepost",
|
|
486
|
+
"compression", # urllib3 submodules
|
|
487
|
+
"backports",
|
|
488
|
+
"base",
|
|
489
|
+
"http2", # httpcore/httpx submodules
|
|
490
|
+
"lib",
|
|
491
|
+
"context",
|
|
492
|
+
"matrixlib", # numpy submodules
|
|
493
|
+
}
|
|
494
|
+
submodule_matches = [
|
|
495
|
+
imp for imp in potentially_missing if imp in known_submodules
|
|
496
|
+
]
|
|
497
|
+
if submodule_matches:
|
|
498
|
+
sections.append(
|
|
499
|
+
f"\nLikely sub-modules (not standalone packages): {', '.join(submodule_matches[:10])}\n"
|
|
500
|
+
)
|
|
501
|
+
else:
|
|
502
|
+
sections.append("\nAll public imports appear to be resolved.\n")
|
|
503
|
+
|
|
504
|
+
if import_errors:
|
|
505
|
+
sections.append(
|
|
506
|
+
f"\nErrors while analyzing imports ({len(import_errors)}):\n"
|
|
507
|
+
)
|
|
508
|
+
for err in import_errors[:10]:
|
|
509
|
+
sections.append(f" {err['file']}: {err['error']}\n")
|
|
510
|
+
|
|
511
|
+
# 3. Python Environment
|
|
512
|
+
sections.append("\n## Python Environment ##\n")
|
|
513
|
+
python_path = ctx.tools.python
|
|
514
|
+
if python_path:
|
|
515
|
+
try:
|
|
516
|
+
py_version = subprocess.run( # nosec B603
|
|
517
|
+
[python_path, "-V"],
|
|
518
|
+
capture_output=True,
|
|
519
|
+
text=True,
|
|
520
|
+
timeout=5,
|
|
521
|
+
check=False,
|
|
522
|
+
)
|
|
523
|
+
if py_version.returncode == 0:
|
|
524
|
+
sections.append(f"Python version: {py_version.stdout.strip()}\n")
|
|
525
|
+
|
|
526
|
+
pip_version = subprocess.run( # nosec B603
|
|
527
|
+
[python_path, "-m", "pip", "--version"],
|
|
528
|
+
capture_output=True,
|
|
529
|
+
text=True,
|
|
530
|
+
timeout=5,
|
|
531
|
+
check=False,
|
|
532
|
+
)
|
|
533
|
+
if pip_version.returncode == 0:
|
|
534
|
+
sections.append(f"Pip: {pip_version.stdout.strip()}\n")
|
|
535
|
+
|
|
536
|
+
# Get count of installed packages
|
|
537
|
+
pip_list = subprocess.run( # nosec B603
|
|
538
|
+
[python_path, "-m", "pip", "list", "--format=json"],
|
|
539
|
+
capture_output=True,
|
|
540
|
+
text=True,
|
|
541
|
+
timeout=10,
|
|
542
|
+
check=False,
|
|
543
|
+
)
|
|
544
|
+
if pip_list.returncode == 0:
|
|
545
|
+
import json
|
|
546
|
+
|
|
547
|
+
packages = json.loads(pip_list.stdout)
|
|
548
|
+
sections.append(f"Installed packages: {len(packages)}\n")
|
|
549
|
+
|
|
550
|
+
except Exception as e:
|
|
551
|
+
sections.append(f"Error getting environment info: {e}\n")
|
|
552
|
+
else:
|
|
553
|
+
sections.append("Python executable not found in PATH\n")
|
|
554
|
+
|
|
555
|
+
# Write the collected output
|
|
556
|
+
text = "".join(sections)
|
|
557
|
+
out.write_text(ctx.redact_text(text), encoding="utf-8")
|
|
558
|
+
|
|
559
|
+
dur = int(time.time() - start)
|
|
560
|
+
note = "issues found" if has_issues else ""
|
|
561
|
+
|
|
562
|
+
return StepResult(self.name, "PASS", dur, note)
|
pybundle/steps/pytest.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
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_tests(root: Path) -> bool:
|
|
14
|
+
# common conventions
|
|
15
|
+
if (root / "tests").is_dir():
|
|
16
|
+
return True
|
|
17
|
+
# sometimes tests are inside the package
|
|
18
|
+
# (don’t walk the whole tree; just check a couple likely paths)
|
|
19
|
+
for candidate in ["src/tests", "app/tests"]:
|
|
20
|
+
if (root / candidate).is_dir():
|
|
21
|
+
return True
|
|
22
|
+
# any */tests at depth 2 is also a common pattern
|
|
23
|
+
for p in root.glob("*/tests"):
|
|
24
|
+
if p.is_dir():
|
|
25
|
+
return True
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class PytestStep:
|
|
31
|
+
name: str = "pytest"
|
|
32
|
+
args: list[str] | None = None
|
|
33
|
+
outfile: str = "logs/34_pytest_q.txt"
|
|
34
|
+
|
|
35
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
36
|
+
start = time.time()
|
|
37
|
+
out = ctx.workdir / self.outfile
|
|
38
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
pytest_bin = which("pytest")
|
|
41
|
+
if not pytest_bin:
|
|
42
|
+
out.write_text(
|
|
43
|
+
"pytest not found; skipping (pip install pytest)\n", encoding="utf-8"
|
|
44
|
+
)
|
|
45
|
+
return StepResult(self.name, "SKIP", 0, "missing pytest")
|
|
46
|
+
|
|
47
|
+
if not _has_tests(ctx.root):
|
|
48
|
+
out.write_text(
|
|
49
|
+
"no tests directory detected; skipping pytest\n", encoding="utf-8"
|
|
50
|
+
)
|
|
51
|
+
return StepResult(self.name, "SKIP", 0, "no tests")
|
|
52
|
+
|
|
53
|
+
args = self.args or ["-q"]
|
|
54
|
+
cmd = [pytest_bin, *args]
|
|
55
|
+
|
|
56
|
+
header = f"## PWD: {ctx.root}\n## CMD: {' '.join(cmd)}\n\n"
|
|
57
|
+
|
|
58
|
+
cp = subprocess.run( # nosec B603
|
|
59
|
+
cmd, cwd=str(ctx.root), text=True, capture_output=True, check=False
|
|
60
|
+
)
|
|
61
|
+
text = header + (cp.stdout or "") + ("\n" + cp.stderr if cp.stderr else "")
|
|
62
|
+
out.write_text(ctx.redact_text(text), encoding="utf-8")
|
|
63
|
+
|
|
64
|
+
dur = int(time.time() - start)
|
|
65
|
+
note = "" if cp.returncode == 0 else f"exit={cp.returncode} (test failures)"
|
|
66
|
+
return StepResult(self.name, "PASS", dur, note)
|