pyvbaharness 1.0.0__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.
@@ -0,0 +1,66 @@
1
+ """pyVBAharness: a hang-resistant harness for running VBA in desktop Excel.
2
+
3
+ Requires Windows, desktop Excel, and the Excel option "Trust access to the
4
+ VBA project object model" (File > Options > Trust Center > Trust Center
5
+ Settings > Macro Settings). See README.md.
6
+ """
7
+ from .numbering import (
8
+ add_line_numbers,
9
+ instrument_error_lines,
10
+ instrument_module,
11
+ )
12
+ from .results import (
13
+ COMPILE_ACCEPTED,
14
+ COMPILE_INFRA_FAILURE,
15
+ COMPILE_REJECTED,
16
+ MODAL_BLOCKED,
17
+ PASSED,
18
+ RUNNER_ERROR,
19
+ TIMEOUT,
20
+ VBA_ERROR,
21
+ CompileResult,
22
+ CoverageReport,
23
+ DialogRecord,
24
+ HarnessError,
25
+ ModuleCoverage,
26
+ RunResult,
27
+ SessionDead,
28
+ SessionLockHeld,
29
+ TestCaseResult,
30
+ VbaError,
31
+ WorkerProtocolError,
32
+ )
33
+ from .pool import SessionPool
34
+ from .session import ExcelSession, HarnessConfig, run_vba
35
+
36
+ __version__ = "1.0.0"
37
+
38
+ __all__ = [
39
+ "ExcelSession",
40
+ "SessionPool",
41
+ "HarnessConfig",
42
+ "run_vba",
43
+ "add_line_numbers",
44
+ "instrument_error_lines",
45
+ "instrument_module",
46
+ "RunResult",
47
+ "CompileResult",
48
+ "CoverageReport",
49
+ "ModuleCoverage",
50
+ "DialogRecord",
51
+ "TestCaseResult",
52
+ "VbaError",
53
+ "HarnessError",
54
+ "SessionDead",
55
+ "SessionLockHeld",
56
+ "WorkerProtocolError",
57
+ "PASSED",
58
+ "VBA_ERROR",
59
+ "TIMEOUT",
60
+ "MODAL_BLOCKED",
61
+ "RUNNER_ERROR",
62
+ "COMPILE_ACCEPTED",
63
+ "COMPILE_REJECTED",
64
+ "COMPILE_INFRA_FAILURE",
65
+ "__version__",
66
+ ]
@@ -0,0 +1,258 @@
1
+ """Command-line interface: python -m pyvbaharness <command>.
2
+
3
+ Commands:
4
+ doctor diagnose the environment (add --live for a real smoke run)
5
+ run FILE run a procedure from a .bas/.vba source file
6
+ check FILE... compile-check source files in a fresh workbook
7
+ check --workbook compile-check an existing workbook's VBA project
8
+
9
+ Exit codes: 0 success/accepted, 1 VBA failure/rejected, 2 harness or
10
+ environment failure. A timeout is infrastructure (2), never a verdict on the
11
+ code.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import sys
17
+ import time
18
+ from pathlib import Path
19
+
20
+
21
+ def _print(line: str = "") -> None:
22
+ print(line, flush=True)
23
+
24
+
25
+ # ----- doctor ---------------------------------------------------------------
26
+
27
+ def _read_registry(root, path: str, name: str | None):
28
+ import winreg
29
+
30
+ try:
31
+ with winreg.OpenKey(root, path) as key:
32
+ value, _kind = winreg.QueryValueEx(key, name)
33
+ return value
34
+ except OSError:
35
+ return None
36
+
37
+
38
+ def _doctor_checks() -> list[tuple[str, bool | None, str]]:
39
+ """(label, ok_or_None_for_warn, detail) rows."""
40
+ import winreg
41
+
42
+ rows: list[tuple[str, bool | None, str]] = []
43
+
44
+ ok = sys.platform == "win32"
45
+ rows.append(("Windows platform", ok, sys.platform))
46
+
47
+ try:
48
+ import win32com.client # noqa: F401
49
+ import pythoncom # noqa: F401
50
+ rows.append(("pywin32 importable", True, "ok"))
51
+ except ImportError as err:
52
+ rows.append(("pywin32 importable", False,
53
+ f"{err}; pip install pywin32"))
54
+
55
+ progid = _read_registry(winreg.HKEY_LOCAL_MACHINE,
56
+ r"SOFTWARE\Classes\Excel.Application\CurVer", None)
57
+ rows.append(("Excel installed", progid is not None,
58
+ str(progid or "Excel.Application ProgID not registered")))
59
+
60
+ office_version = ""
61
+ if isinstance(progid, str) and progid.rsplit(".", 1)[-1].isdigit():
62
+ office_version = progid.rsplit(".", 1)[-1] + ".0"
63
+ if office_version:
64
+ access = _read_registry(
65
+ winreg.HKEY_CURRENT_USER,
66
+ rf"Software\Microsoft\Office\{office_version}\Excel\Security",
67
+ "AccessVBOM")
68
+ rows.append((
69
+ "Trust access to the VBA project object model",
70
+ access == 1,
71
+ "enabled" if access == 1 else
72
+ "DISABLED. File > Options > Trust Center > Trust Center "
73
+ "Settings > Macro Settings > tick 'Trust access to the VBA "
74
+ "project object model'."))
75
+ else:
76
+ rows.append(("Trust access to the VBA project object model", None,
77
+ "could not determine the Office version"))
78
+
79
+ # VBE error-trapping mode. 1 = Break on All Errors, which stops in the
80
+ # debugger even for handled errors: every managed run would report
81
+ # modal-blocked instead of vba-error.
82
+ break_mode = None
83
+ for vba_version in ("7.1", "7.0", "6.0"):
84
+ break_mode = _read_registry(
85
+ winreg.HKEY_CURRENT_USER,
86
+ rf"Software\Microsoft\VBA\{vba_version}\Common",
87
+ "BreakOnAllErrors")
88
+ if break_mode is not None:
89
+ break
90
+ if break_mode == 1:
91
+ rows.append(("VBE error trapping", False,
92
+ "'Break on All Errors' is set. In the VBE: Tools > "
93
+ "Options > General > Error Trapping > 'Break on "
94
+ "Unhandled Errors'. With break-on-all, handled VBA "
95
+ "errors stop in the debugger and runs report "
96
+ "modal-blocked."))
97
+ else:
98
+ rows.append(("VBE error trapping", True,
99
+ "break on unhandled errors"))
100
+
101
+ return rows
102
+
103
+
104
+ def cmd_doctor(args: argparse.Namespace) -> int:
105
+ rows = _doctor_checks()
106
+ hard_fail = False
107
+ for label, ok, detail in rows:
108
+ if ok is True:
109
+ mark = "pass"
110
+ elif ok is None:
111
+ mark = "warn"
112
+ else:
113
+ mark = "FAIL"
114
+ hard_fail = True
115
+ _print(f"[{mark}] {label}: {detail}")
116
+
117
+ if args.live and not hard_fail:
118
+ from . import ExcelSession
119
+
120
+ _print()
121
+ _print("Live smoke: starting an owned Excel...")
122
+ started = time.perf_counter()
123
+ try:
124
+ with ExcelSession() as session:
125
+ startup = time.perf_counter() - started
126
+ result = session.run_vba(
127
+ "Public Function PyVbaDoctor() As Long\n"
128
+ " PyVbaDoctor = 42\n"
129
+ "End Function\n", proc="PyVbaDoctor")
130
+ if result.outcome == "passed" and result.value == 42:
131
+ _print(f"[pass] live run: startup {startup:.1f}s, "
132
+ f"run {result.duration_s * 1000:.0f}ms")
133
+ else:
134
+ _print(f"[FAIL] live run: {result.outcome} "
135
+ f"{result.message}")
136
+ hard_fail = True
137
+ except Exception as err: # noqa: BLE001 - report, do not crash
138
+ _print(f"[FAIL] live run: {type(err).__name__}: {err}")
139
+ hard_fail = True
140
+
141
+ return 2 if hard_fail else 0
142
+
143
+
144
+ # ----- run ------------------------------------------------------------------
145
+
146
+ def _parse_cli_arg(text: str):
147
+ for cast in (int, float):
148
+ try:
149
+ return cast(text)
150
+ except ValueError:
151
+ continue
152
+ if text.lower() in ("true", "false"):
153
+ return text.lower() == "true"
154
+ return text
155
+
156
+
157
+ def cmd_run(args: argparse.Namespace) -> int:
158
+ from . import ExcelSession
159
+
160
+ source = Path(args.file).read_text(encoding="utf-8-sig")
161
+ call_args = tuple(_parse_cli_arg(a) for a in args.arg)
162
+ with ExcelSession() as session:
163
+ result = session.run_vba(source, proc=args.proc, args=call_args,
164
+ timeout=args.timeout)
165
+ for line in result.output:
166
+ _print(line)
167
+ if result.outcome == "passed":
168
+ if result.value is not None:
169
+ _print(f"value: {result.value!r}")
170
+ return 0
171
+ if result.error is not None:
172
+ _print(f"error: {result.error}")
173
+ return 1
174
+ _print(f"{result.outcome}: {result.message}")
175
+ return 2
176
+
177
+
178
+ # ----- check ----------------------------------------------------------------
179
+
180
+ def cmd_check(args: argparse.Namespace) -> int:
181
+ from . import ExcelSession
182
+ from .codegen import is_vba_identifier
183
+
184
+ if not args.files and not args.workbook:
185
+ _print("check needs source files or --workbook")
186
+ return 2
187
+ with ExcelSession() as session:
188
+ if args.workbook:
189
+ # Compile the workbook exactly as-is: no injected modules.
190
+ session.open_workbook(args.workbook, read_only=True)
191
+ result = session.compile_project(watch_seconds=args.watch)
192
+ else:
193
+ session.new_workbook()
194
+ for file_name in args.files:
195
+ path = Path(file_name)
196
+ module = path.stem
197
+ if not is_vba_identifier(module):
198
+ _print(f"skipping {path.name}: file stem is not a valid "
199
+ "VBA module name")
200
+ return 2
201
+ kind = "class" if path.suffix.lower() == ".cls" else "standard"
202
+ session.add_module(module,
203
+ path.read_text(encoding="utf-8-sig"),
204
+ kind=kind)
205
+ # Loose files are destined for the harness: compile them with
206
+ # the support module present, the way they will actually run.
207
+ result = session.compile_project(
208
+ watch_seconds=args.watch, include_harness_support=True)
209
+ if result.outcome == "accepted":
210
+ _print(f"accepted in {result.duration_s:.1f}s")
211
+ return 0
212
+ if result.outcome == "rejected":
213
+ _print("rejected: compile error")
214
+ if result.dialog is not None:
215
+ for text in (result.dialog.texts or [result.dialog.message]):
216
+ _print(f" {text}")
217
+ return 1
218
+ _print(f"infrastructure failure: {result.message}")
219
+ return 2
220
+
221
+
222
+ # ----- entry ----------------------------------------------------------------
223
+
224
+ def main(argv: list[str] | None = None) -> int:
225
+ parser = argparse.ArgumentParser(
226
+ prog="python -m pyvbaharness",
227
+ description="Hang-resistant VBA harness for desktop Excel.")
228
+ commands = parser.add_subparsers(dest="command", required=True)
229
+
230
+ doctor = commands.add_parser(
231
+ "doctor", help="diagnose the environment for harness use")
232
+ doctor.add_argument("--live", action="store_true",
233
+ help="also start Excel and run a smoke test")
234
+ doctor.set_defaults(func=cmd_doctor)
235
+
236
+ run = commands.add_parser("run", help="run a procedure from a VBA file")
237
+ run.add_argument("file")
238
+ run.add_argument("--proc", default="Main")
239
+ run.add_argument("--timeout", type=float, default=None)
240
+ run.add_argument("--arg", action="append", default=[],
241
+ help="argument for the procedure (repeatable; int, "
242
+ "float, and true/false are auto-converted)")
243
+ run.set_defaults(func=cmd_run)
244
+
245
+ check = commands.add_parser(
246
+ "check", help="compile-check VBA files or a workbook")
247
+ check.add_argument("files", nargs="*")
248
+ check.add_argument("--workbook", default=None)
249
+ check.add_argument("--watch", type=float, default=15.0,
250
+ help="dialog watch window in seconds (default 15)")
251
+ check.set_defaults(func=cmd_check)
252
+
253
+ args = parser.parse_args(argv)
254
+ return args.func(args)
255
+
256
+
257
+ if __name__ == "__main__":
258
+ sys.exit(main())