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.

Files changed (82) hide show
  1. gwc_pybundle-2.1.2.dist-info/METADATA +903 -0
  2. gwc_pybundle-2.1.2.dist-info/RECORD +82 -0
  3. gwc_pybundle-2.1.2.dist-info/WHEEL +5 -0
  4. gwc_pybundle-2.1.2.dist-info/entry_points.txt +2 -0
  5. gwc_pybundle-2.1.2.dist-info/licenses/LICENSE.md +25 -0
  6. gwc_pybundle-2.1.2.dist-info/top_level.txt +1 -0
  7. pybundle/__init__.py +0 -0
  8. pybundle/__main__.py +4 -0
  9. pybundle/cli.py +546 -0
  10. pybundle/context.py +404 -0
  11. pybundle/doctor.py +148 -0
  12. pybundle/filters.py +228 -0
  13. pybundle/manifest.py +77 -0
  14. pybundle/packaging.py +45 -0
  15. pybundle/policy.py +132 -0
  16. pybundle/profiles.py +454 -0
  17. pybundle/roadmap_model.py +42 -0
  18. pybundle/roadmap_scan.py +328 -0
  19. pybundle/root_detect.py +14 -0
  20. pybundle/runner.py +180 -0
  21. pybundle/steps/__init__.py +26 -0
  22. pybundle/steps/ai_context.py +791 -0
  23. pybundle/steps/api_docs.py +219 -0
  24. pybundle/steps/asyncio_analysis.py +358 -0
  25. pybundle/steps/bandit.py +72 -0
  26. pybundle/steps/base.py +20 -0
  27. pybundle/steps/blocking_call_detection.py +291 -0
  28. pybundle/steps/call_graph.py +219 -0
  29. pybundle/steps/compileall.py +76 -0
  30. pybundle/steps/config_docs.py +319 -0
  31. pybundle/steps/config_validation.py +302 -0
  32. pybundle/steps/container_image.py +294 -0
  33. pybundle/steps/context_expand.py +272 -0
  34. pybundle/steps/copy_pack.py +293 -0
  35. pybundle/steps/coverage.py +101 -0
  36. pybundle/steps/cprofile_step.py +166 -0
  37. pybundle/steps/dependency_sizes.py +136 -0
  38. pybundle/steps/django_checks.py +214 -0
  39. pybundle/steps/dockerfile_lint.py +282 -0
  40. pybundle/steps/dockerignore.py +311 -0
  41. pybundle/steps/duplication.py +103 -0
  42. pybundle/steps/env_completeness.py +269 -0
  43. pybundle/steps/env_var_usage.py +253 -0
  44. pybundle/steps/error_refs.py +204 -0
  45. pybundle/steps/event_loop_patterns.py +280 -0
  46. pybundle/steps/exception_patterns.py +190 -0
  47. pybundle/steps/fastapi_integration.py +250 -0
  48. pybundle/steps/flask_debugging.py +312 -0
  49. pybundle/steps/git_analytics.py +315 -0
  50. pybundle/steps/handoff_md.py +176 -0
  51. pybundle/steps/import_time.py +175 -0
  52. pybundle/steps/interrogate.py +106 -0
  53. pybundle/steps/license_scan.py +96 -0
  54. pybundle/steps/line_profiler.py +117 -0
  55. pybundle/steps/link_validation.py +287 -0
  56. pybundle/steps/logging_analysis.py +233 -0
  57. pybundle/steps/memory_profile.py +176 -0
  58. pybundle/steps/migration_history.py +336 -0
  59. pybundle/steps/mutation_testing.py +141 -0
  60. pybundle/steps/mypy.py +103 -0
  61. pybundle/steps/orm_optimization.py +316 -0
  62. pybundle/steps/pip_audit.py +45 -0
  63. pybundle/steps/pipdeptree.py +62 -0
  64. pybundle/steps/pylance.py +562 -0
  65. pybundle/steps/pytest.py +66 -0
  66. pybundle/steps/query_pattern_analysis.py +334 -0
  67. pybundle/steps/radon.py +161 -0
  68. pybundle/steps/repro_md.py +161 -0
  69. pybundle/steps/rg_scans.py +78 -0
  70. pybundle/steps/roadmap.py +153 -0
  71. pybundle/steps/ruff.py +117 -0
  72. pybundle/steps/secrets_detection.py +235 -0
  73. pybundle/steps/security_headers.py +309 -0
  74. pybundle/steps/shell.py +74 -0
  75. pybundle/steps/slow_tests.py +178 -0
  76. pybundle/steps/sqlalchemy_validation.py +269 -0
  77. pybundle/steps/test_flakiness.py +184 -0
  78. pybundle/steps/tree.py +116 -0
  79. pybundle/steps/type_coverage.py +277 -0
  80. pybundle/steps/unused_deps.py +211 -0
  81. pybundle/steps/vulture.py +167 -0
  82. 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)
@@ -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)