pyOpenVBA 2.0.1__tar.gz → 3.0.1__tar.gz
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.
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/.gitignore +5 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/PKG-INFO +62 -5
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/README.md +61 -4
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/docs/architecture.md +108 -1
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/pyproject.toml +35 -1
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/__init__.py +53 -29
- pyopenvba-3.0.1/src/pyopenvba/__main__.py +216 -0
- pyopenvba-3.0.1/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- pyopenvba-3.0.1/src/pyopenvba/access_read.py +1728 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/cfb.py +11 -10
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/excel.py +10 -9
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/powerpoint.py +10 -9
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/vba.py +62 -25
- pyopenvba-3.0.1/src/pyopenvba/vba_pcode.py +736 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/word.py +10 -9
- pyopenvba-2.0.1/src/pyopenvba/__main__.py +0 -68
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/LICENSE.md +0 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/docs/ms-ovba-implementation-guide_v2.md +0 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/docs/roadmap.md +0 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/__init__.py +0 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-2.0.1 → pyopenvba-3.0.1}/tests/fuzz_corpus/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyOpenVBA
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: Read and write VBA macros inside Excel, Word, and PowerPoint files in pure Python, no dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
6
6
|
Project-URL: Repository, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
@@ -40,9 +40,9 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
[](https://pypi.org/project/pyOpenVBA/)
|
|
41
41
|
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
|
|
42
42
|
[](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
|
|
43
|
-
[](https://pepy.tech/project/pyOpenVBA)
|
|
44
44
|
|
|
45
|
-
**Read and write VBA macros inside
|
|
45
|
+
**Read and write VBA macros inside Office 365 files, in pure Python.**
|
|
46
46
|
|
|
47
47
|
No external dependencies. No Office install required. Works on Windows,
|
|
48
48
|
macOS, and Linux. Python 3.10 or newer.
|
|
@@ -52,6 +52,7 @@ Supports:
|
|
|
52
52
|
* Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`)
|
|
53
53
|
* PowerPoint (`.pptm`, `.potm`, `.ppt`)
|
|
54
54
|
* Word (`.docm`, `.dotm`, `.doc`)
|
|
55
|
+
* Access (`.accdb`) - **read-only**
|
|
55
56
|
|
|
56
57
|
<a href="https://github.com/sponsors/WilliamSmithEdward"><img src="https://img.shields.io/badge/Sponsor-%E2%9D%A4-pink?style=for-the-badge" alt="Sponsor WilliamSmithEdward"></a>
|
|
57
58
|
|
|
@@ -170,8 +171,25 @@ with PowerPointFile("presentation.pptm") as prs:
|
|
|
170
171
|
prs.save()
|
|
171
172
|
```
|
|
172
173
|
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
### Access (read-only)
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from pyopenvba import AccessReader
|
|
178
|
+
|
|
179
|
+
with AccessReader("database.accdb") as db:
|
|
180
|
+
# 1. List all VBA modules in the database.
|
|
181
|
+
modules = db.vba_modules()
|
|
182
|
+
print(list(modules))
|
|
183
|
+
# ['Module1', 'Form_Form1']
|
|
184
|
+
|
|
185
|
+
# 2. Read a module's source as a string.
|
|
186
|
+
source = db.get_module("Module1")
|
|
187
|
+
print(source)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Excel, Word, and PowerPoint share the same read/write API:
|
|
191
|
+
`module_names()`, `get_module()`, `set_module()`, `save()`. Access is
|
|
192
|
+
currently read-only and exposes `vba_modules()` and `get_module()`.
|
|
175
193
|
|
|
176
194
|
---
|
|
177
195
|
|
|
@@ -326,6 +344,45 @@ modules, `.cls` for class modules and code-behind.
|
|
|
326
344
|
| `.potm` | Macro-enabled template | yes | yes | no |
|
|
327
345
|
| `.ppt` | Legacy (PowerPoint 97-2003) | yes | yes | no |
|
|
328
346
|
|
|
347
|
+
### Access (read-only)
|
|
348
|
+
|
|
349
|
+
| Extension | What it is | Read | Write | create_new |
|
|
350
|
+
|-----------|------------------------------|:----:|:-----:|:----------:|
|
|
351
|
+
| `.accdb` | Access database (ACE engine) | yes | no | no |
|
|
352
|
+
|
|
353
|
+
Access stores compiled VBA p-code (the `rU@` + `CAFE` rows in the LVAL
|
|
354
|
+
catalog) separately from the OVBA source cache. The compiled p-code is
|
|
355
|
+
authoritative for the Access GUI; mutations to the source cache do not
|
|
356
|
+
survive reload because Access never recompiles from the cache. After
|
|
357
|
+
extensive reverse-engineering experiments we concluded that a
|
|
358
|
+
production-quality writer would require a complete VBA7 p-code
|
|
359
|
+
assembler, which is out of scope. See
|
|
360
|
+
[docs/msaccess_lessons_learned.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/msaccess_lessons_learned.md)
|
|
361
|
+
for the full chronicle.
|
|
362
|
+
|
|
363
|
+
What `AccessReader` does support:
|
|
364
|
+
|
|
365
|
+
- `AccessReader(path)` / `vba_module_names()` / `read_vba_module(name)`
|
|
366
|
+
- `read_vba_module_with_attributes(name)`
|
|
367
|
+
- `vba_modules()` (dict of name -> source)
|
|
368
|
+
- `iter_vba_modules()` (rich `VBAModule` records)
|
|
369
|
+
- `export_module()` / `export_modules()` / `pull_modules()` (write `.bas` / `.cls` to disk)
|
|
370
|
+
- `read_project_info()`, `identifiers()`, `find_interned_strings()`,
|
|
371
|
+
`find_module_streams()`, `iter_pcode_streams()`, `disassemble_module()`
|
|
372
|
+
- `iter_msys_objects()` / `msys_objects()` / `iter_msys_modules()` /
|
|
373
|
+
`find_msys_module()` (MSysObjects catalog inspection)
|
|
374
|
+
- Top-level helper: `pyopenvba.pull_access(database, dest_dir)`
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
from pyopenvba import AccessReader, pull_access
|
|
378
|
+
|
|
379
|
+
with AccessReader("database.accdb") as db:
|
|
380
|
+
for name, source in db.vba_modules().items():
|
|
381
|
+
print(name, len(source))
|
|
382
|
+
|
|
383
|
+
pull_access("database.accdb", "./vba_src") # export every module to .bas / .cls
|
|
384
|
+
```
|
|
385
|
+
|
|
329
386
|
Every save is verified to reopen in the host application **without** the
|
|
330
387
|
"we found a problem with some content" repair dialog.
|
|
331
388
|
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
[](https://pypi.org/project/pyOpenVBA/)
|
|
5
5
|
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
|
|
6
6
|
[](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
|
|
7
|
-
[](https://pepy.tech/project/pyOpenVBA)
|
|
8
8
|
|
|
9
|
-
**Read and write VBA macros inside
|
|
9
|
+
**Read and write VBA macros inside Office 365 files, in pure Python.**
|
|
10
10
|
|
|
11
11
|
No external dependencies. No Office install required. Works on Windows,
|
|
12
12
|
macOS, and Linux. Python 3.10 or newer.
|
|
@@ -16,6 +16,7 @@ Supports:
|
|
|
16
16
|
* Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`)
|
|
17
17
|
* PowerPoint (`.pptm`, `.potm`, `.ppt`)
|
|
18
18
|
* Word (`.docm`, `.dotm`, `.doc`)
|
|
19
|
+
* Access (`.accdb`) - **read-only**
|
|
19
20
|
|
|
20
21
|
<a href="https://github.com/sponsors/WilliamSmithEdward"><img src="https://img.shields.io/badge/Sponsor-%E2%9D%A4-pink?style=for-the-badge" alt="Sponsor WilliamSmithEdward"></a>
|
|
21
22
|
|
|
@@ -134,8 +135,25 @@ with PowerPointFile("presentation.pptm") as prs:
|
|
|
134
135
|
prs.save()
|
|
135
136
|
```
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
### Access (read-only)
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from pyopenvba import AccessReader
|
|
142
|
+
|
|
143
|
+
with AccessReader("database.accdb") as db:
|
|
144
|
+
# 1. List all VBA modules in the database.
|
|
145
|
+
modules = db.vba_modules()
|
|
146
|
+
print(list(modules))
|
|
147
|
+
# ['Module1', 'Form_Form1']
|
|
148
|
+
|
|
149
|
+
# 2. Read a module's source as a string.
|
|
150
|
+
source = db.get_module("Module1")
|
|
151
|
+
print(source)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Excel, Word, and PowerPoint share the same read/write API:
|
|
155
|
+
`module_names()`, `get_module()`, `set_module()`, `save()`. Access is
|
|
156
|
+
currently read-only and exposes `vba_modules()` and `get_module()`.
|
|
139
157
|
|
|
140
158
|
---
|
|
141
159
|
|
|
@@ -290,6 +308,45 @@ modules, `.cls` for class modules and code-behind.
|
|
|
290
308
|
| `.potm` | Macro-enabled template | yes | yes | no |
|
|
291
309
|
| `.ppt` | Legacy (PowerPoint 97-2003) | yes | yes | no |
|
|
292
310
|
|
|
311
|
+
### Access (read-only)
|
|
312
|
+
|
|
313
|
+
| Extension | What it is | Read | Write | create_new |
|
|
314
|
+
|-----------|------------------------------|:----:|:-----:|:----------:|
|
|
315
|
+
| `.accdb` | Access database (ACE engine) | yes | no | no |
|
|
316
|
+
|
|
317
|
+
Access stores compiled VBA p-code (the `rU@` + `CAFE` rows in the LVAL
|
|
318
|
+
catalog) separately from the OVBA source cache. The compiled p-code is
|
|
319
|
+
authoritative for the Access GUI; mutations to the source cache do not
|
|
320
|
+
survive reload because Access never recompiles from the cache. After
|
|
321
|
+
extensive reverse-engineering experiments we concluded that a
|
|
322
|
+
production-quality writer would require a complete VBA7 p-code
|
|
323
|
+
assembler, which is out of scope. See
|
|
324
|
+
[docs/msaccess_lessons_learned.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/msaccess_lessons_learned.md)
|
|
325
|
+
for the full chronicle.
|
|
326
|
+
|
|
327
|
+
What `AccessReader` does support:
|
|
328
|
+
|
|
329
|
+
- `AccessReader(path)` / `vba_module_names()` / `read_vba_module(name)`
|
|
330
|
+
- `read_vba_module_with_attributes(name)`
|
|
331
|
+
- `vba_modules()` (dict of name -> source)
|
|
332
|
+
- `iter_vba_modules()` (rich `VBAModule` records)
|
|
333
|
+
- `export_module()` / `export_modules()` / `pull_modules()` (write `.bas` / `.cls` to disk)
|
|
334
|
+
- `read_project_info()`, `identifiers()`, `find_interned_strings()`,
|
|
335
|
+
`find_module_streams()`, `iter_pcode_streams()`, `disassemble_module()`
|
|
336
|
+
- `iter_msys_objects()` / `msys_objects()` / `iter_msys_modules()` /
|
|
337
|
+
`find_msys_module()` (MSysObjects catalog inspection)
|
|
338
|
+
- Top-level helper: `pyopenvba.pull_access(database, dest_dir)`
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
from pyopenvba import AccessReader, pull_access
|
|
342
|
+
|
|
343
|
+
with AccessReader("database.accdb") as db:
|
|
344
|
+
for name, source in db.vba_modules().items():
|
|
345
|
+
print(name, len(source))
|
|
346
|
+
|
|
347
|
+
pull_access("database.accdb", "./vba_src") # export every module to .bas / .cls
|
|
348
|
+
```
|
|
349
|
+
|
|
293
350
|
Every save is verified to reopen in the host application **without** the
|
|
294
351
|
"we found a problem with some content" repair dialog.
|
|
295
352
|
|
|
@@ -33,12 +33,34 @@ knows about the layer below it but not the layer above.
|
|
|
33
33
|
| - identical save() pipeline as ExcelFile |
|
|
34
34
|
| - pull / push disk workflow |
|
|
35
35
|
+--------------------------------------------------------+
|
|
36
|
+
| access_read.py AccessReader facade (READ-ONLY) |
|
|
37
|
+
| - ACE / Jet 4 page reader (.accdb, .mdb) |
|
|
38
|
+
| - MS-OVBA blob discovery via signature scan |
|
|
39
|
+
| - LVAL page-chain walker |
|
|
40
|
+
| - read_vba_module(name) -> str (pure Python) |
|
|
41
|
+
| - get_module / vba_modules / iter_vba_modules |
|
|
42
|
+
| - pull_modules / export_modules / export_module |
|
|
43
|
+
| - read_vba_module_with_attributes (full preamble) |
|
|
44
|
+
| - read_project_info / identifiers |
|
|
45
|
+
| - find_interned_strings / find_module_streams |
|
|
46
|
+
| - iter_pcode_streams / read_module_pcode_stream |
|
|
47
|
+
| - disassemble_module (via vba_pcode) |
|
|
48
|
+
| - iter_msys_objects / find_msys_module |
|
|
49
|
+
| (MSysObjects system catalog reader) |
|
|
50
|
+
| - NO write APIs; see msaccess_lessons_learned.md |
|
|
51
|
+
+--------------------------------------------------------+
|
|
36
52
|
| vba.py VBA project layer |
|
|
37
53
|
| - MS-OVBA compression / decompression |
|
|
38
54
|
| - dir / PROJECT / PROJECTwm / PROJECTlk parsers |
|
|
39
55
|
| - VBAProject + VBAModule data model |
|
|
40
56
|
| - cache invalidation, signature detection |
|
|
41
57
|
+--------------------------------------------------------+
|
|
58
|
+
| vba_pcode.py VBA7 p-code disassembler (EXPERIMENTAL)|
|
|
59
|
+
| - 264-entry VBA7 opcode table (factual data) |
|
|
60
|
+
| - per-line p-code streaming parser |
|
|
61
|
+
| - source-line correlation (to_annotated_listing) |
|
|
62
|
+
| - dependency-free (no oletools, no pcodedmp) |
|
|
63
|
+
+--------------------------------------------------------+
|
|
42
64
|
| cfb.py MS-CFB layer |
|
|
43
65
|
| - Compound File Binary parser/writer |
|
|
44
66
|
| - stream + storage CRUD (case-insensitive lookup) |
|
|
@@ -50,7 +72,10 @@ Other files:
|
|
|
50
72
|
- `exceptions.py` — exception hierarchy shared by all layers.
|
|
51
73
|
- `__init__.py` — public re-exports (`ExcelFile`, `pull`, `push`,
|
|
52
74
|
`VBAModuleKind`, exceptions).
|
|
53
|
-
- `__main__.py` — `python -m pyopenvba
|
|
75
|
+
- `__main__.py` — `python -m pyopenvba
|
|
76
|
+
{pull,push,ls,access-ls,access-pull,access-disasm,disasm}` CLI.
|
|
77
|
+
`disasm` / `access-disasm` accept `--with-source` to interleave
|
|
78
|
+
the original VBA source with the decoded p-code.
|
|
54
79
|
- `_templates/__init__.py` — generated module embedding a
|
|
55
80
|
zlib-compressed base85 blob of a freshly Excel-authored empty `.xlsm`,
|
|
56
81
|
consumed by `ExcelFile.create_new()`. Regenerated from
|
|
@@ -154,6 +179,84 @@ If any of steps 3-10 fail, the on-disk file is **never modified** —
|
|
|
154
179
|
all work is done on the in-memory CFB and the final bytes are written
|
|
155
180
|
in one atomic-ish operation.
|
|
156
181
|
|
|
182
|
+
### 3.3 Access read-only model (`AccessReader`)
|
|
183
|
+
|
|
184
|
+
`.accdb`/`.mdb` files are exposed through a **read-only** facade.
|
|
185
|
+
After an extensive reverse-engineering effort (chronicled in
|
|
186
|
+
[docs/msaccess_lessons_learned.md](msaccess_lessons_learned.md))
|
|
187
|
+
pyOpenVBA does not support writing back to Access databases. Use
|
|
188
|
+
Access COM (`win32com.client.Dispatch("Access.Application")`) if you
|
|
189
|
+
need to programmatically modify VBA inside a `.accdb`.
|
|
190
|
+
|
|
191
|
+
The Access on-disk layout differs from Excel/Word/PowerPoint:
|
|
192
|
+
|
|
193
|
+
* The MS-OVBA blob on the LVAL chain is a **passive cache**. Zero-filling
|
|
194
|
+
the entire blob has no effect on what Access (or the VBA editor)
|
|
195
|
+
displays — verified end-to-end on a live fixture. This is what
|
|
196
|
+
pyOpenVBA reads.
|
|
197
|
+
* The **authoritative** sources Access reads from are:
|
|
198
|
+
* **Project metadata** — the MS-OVBA `dir` stream (Section 2.3.4.2)
|
|
199
|
+
OVBA-compressed in a single LVAL row; located by content
|
|
200
|
+
(decompressed prefix = `01 00 04 00 00 00`). Parsed by
|
|
201
|
+
`AccessReader.read_project_info()` -> `AccessVBAProject` (system kind,
|
|
202
|
+
LCID, code page, project name, references, modules with class flag,
|
|
203
|
+
private/read-only flags).
|
|
204
|
+
* **Module bytecode** — the compiled VBA p-code. Lives in LVAL rows
|
|
205
|
+
whose payload starts with magic `72 55 40 00` ('rU@\0'). The
|
|
206
|
+
**module-active** one is identified deterministically by the
|
|
207
|
+
12-byte prefix `72 55 40 00 00 00 00 00 00 00 40 00`
|
|
208
|
+
(byte 10 = 0x40). Exposed via
|
|
209
|
+
`AccessReader.read_module_pcode_stream()` and `iter_pcode_streams()`.
|
|
210
|
+
Compiled p-code is **fully anonymised**: opcodes and slot
|
|
211
|
+
references with no user-authored text. The standard MS-OVBA
|
|
212
|
+
`0xCAFE` module stream coexists alongside `rU@` in a separate
|
|
213
|
+
LVAL row and is what `AccessReader.disassemble_module()` consumes
|
|
214
|
+
via the pure-Python `pyopenvba.vba_pcode` decoder.
|
|
215
|
+
* **Identifier inventory** — every project-level identifier name
|
|
216
|
+
(typelib refs, project name, module/proc/variable names, intrinsic
|
|
217
|
+
call targets such as `MsgBox`) is enumerated in the
|
|
218
|
+
`_VBA_PROJECT`-equivalent stream stored UNCOMPRESSED in the LVAL
|
|
219
|
+
row whose first 2 bytes are the `CC 61` Office magic. Exposed via
|
|
220
|
+
`AccessReader.identifiers() -> tuple[AccessVBAIdentifier, ...]`.
|
|
221
|
+
* **Comment text** — stored verbatim in plaintext rows tagged
|
|
222
|
+
`E3 00 00 00 <u16-LE length> <ASCII payload>` (apostrophe stripped).
|
|
223
|
+
* **String literal text** — stored verbatim in rows tagged
|
|
224
|
+
`B9 00 <u16-LE length> <ASCII payload> <12-byte trailer>`,
|
|
225
|
+
exposed via `AccessReader.find_interned_strings()`.
|
|
226
|
+
|
|
227
|
+
The **MSysObjects** system catalog is also surfaced read-only:
|
|
228
|
+
`AccessReader.iter_msys_objects()`, `msys_objects()`,
|
|
229
|
+
`iter_msys_modules()`, `find_msys_object(name, *, type_=None)`,
|
|
230
|
+
`find_msys_module(name)`. Each row yields an `AccessSysObject`
|
|
231
|
+
dataclass (id_, parent_id, type_, flags, name, page, slot). VBA code
|
|
232
|
+
modules carry `type_ == MSYS_TYPE_MODULE` (-32761, 0x8007) and are
|
|
233
|
+
parented to the `Modules` container row.
|
|
234
|
+
|
|
235
|
+
### Excel-symmetric VBA module read API
|
|
236
|
+
|
|
237
|
+
`AccessReader` exposes the same ergonomic read surface as `ExcelFile` /
|
|
238
|
+
`WordFile` / `PowerPointFile`: `get_module(name)`,
|
|
239
|
+
`vba_modules() -> dict[str, str]`, `read_vba_module(name)`,
|
|
240
|
+
`read_vba_module_with_attributes(name)`, `iter_vba_modules()`,
|
|
241
|
+
`pull_modules(dest_dir)`, `export_module(name)`,
|
|
242
|
+
`export_modules(dest_dir, *, include_attributes=False)`. The
|
|
243
|
+
top-level helper `pyopenvba.pull_access(database, dest_dir)` mirrors
|
|
244
|
+
`pull` / `pull_word` / `pull_ppt`. There is intentionally no
|
|
245
|
+
`push_access` symbol.
|
|
246
|
+
|
|
247
|
+
### Why no write path
|
|
248
|
+
|
|
249
|
+
Access stores compiled VBA p-code (the `rU@` + `CAFE` rows) separately
|
|
250
|
+
from the OVBA source cache. The compiled p-code is authoritative for
|
|
251
|
+
the Access GUI; mutations to the source cache do not survive reload
|
|
252
|
+
because Access never recompiles from the cache. We could not locate a
|
|
253
|
+
recompile trigger. A production-quality writer would require a full
|
|
254
|
+
VBA7 p-code assembler (instruction emission, identifier table
|
|
255
|
+
re-indexing, jump fixup, MSysObjects long-value chunk reflow, ACE
|
|
256
|
+
page allocator parity). That is out of scope. See
|
|
257
|
+
[docs/msaccess_lessons_learned.md](msaccess_lessons_learned.md) for
|
|
258
|
+
the empirical results matrix and the reasoning in full.
|
|
259
|
+
|
|
157
260
|
---
|
|
158
261
|
|
|
159
262
|
## 4. Mutation safety gates
|
|
@@ -256,10 +359,14 @@ tests/
|
|
|
256
359
|
test_powerpoint.py PowerPointFile facade, end-to-end
|
|
257
360
|
test_pull_push.py Disk workflow
|
|
258
361
|
test_gates.py Per-roadmap-gate regression tests
|
|
362
|
+
test_access.py AccessReader read path: page walk,
|
|
363
|
+
LVAL chain walk, MS-OVBA blob decode,
|
|
364
|
+
byte-for-byte oracle parity (EXPERIMENTAL)
|
|
259
365
|
fuzz_corpus/ Persistent fuzz seeds (see test_gates.py Gate 23)
|
|
260
366
|
live_excel_testing/ Real fixture workbooks (Excel)
|
|
261
367
|
live_word_testing/ Real fixture documents (Word)
|
|
262
368
|
live_powerpoint_testing/ Real fixture presentations (PowerPoint)
|
|
369
|
+
live_access_test/ Real fixture databases (Access) + COM oracle
|
|
263
370
|
```
|
|
264
371
|
|
|
265
372
|
- **Always** run pytest with `-p no:randomly` to keep ordering
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyOpenVBA"
|
|
7
|
-
version = "
|
|
7
|
+
version = "3.0.1"
|
|
8
8
|
description = "Read and write VBA macros inside Excel, Word, and PowerPoint files in pure Python, no dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -86,3 +86,37 @@ testpaths = ["tests"]
|
|
|
86
86
|
include = ["src", "tests"]
|
|
87
87
|
pythonVersion = "3.10"
|
|
88
88
|
typeCheckingMode = "strict"
|
|
89
|
+
|
|
90
|
+
[tool.ruff]
|
|
91
|
+
line-length = 120
|
|
92
|
+
target-version = "py310"
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint]
|
|
95
|
+
select = ["E", "F", "W", "B", "UP", "SIM", "I", "RUF", "PIE", "C4", "PERF", "N", "TC", "RET", "TRY"]
|
|
96
|
+
# Ignore rules that conflict with the project's strict-pyright typing posture,
|
|
97
|
+
# intentional MS-spec naming, or stylistic preferences:
|
|
98
|
+
# B009 getattr(obj, "_private") -- deliberate to bypass reportPrivateUsage in tests
|
|
99
|
+
# PIE807 default_factory=lambda: [] -- pyright strict cannot infer element type from bare `list`
|
|
100
|
+
# TRY003 raising vanilla exception messages is fine in this project
|
|
101
|
+
# E501 long lines are tolerated where they aid readability of binary specs
|
|
102
|
+
# N801/N802/N806 mixed-case names mirror MS-OVBA spec record/field names
|
|
103
|
+
# PERF203 try/except inside loops is required for per-row recovery in parsers
|
|
104
|
+
# PERF401 manual list builds are clearer than comprehensions in spec-driven code
|
|
105
|
+
# SIM105 contextlib.suppress is slower; explicit try/except is preferred
|
|
106
|
+
# TRY300 try-else is stylistic
|
|
107
|
+
# TC001/TC003 moving runtime-required imports into TYPE_CHECKING risks breakage
|
|
108
|
+
ignore = [
|
|
109
|
+
"B009",
|
|
110
|
+
"PIE807",
|
|
111
|
+
"TRY003",
|
|
112
|
+
"E501",
|
|
113
|
+
"N801",
|
|
114
|
+
"N802",
|
|
115
|
+
"N806",
|
|
116
|
+
"PERF203",
|
|
117
|
+
"PERF401",
|
|
118
|
+
"SIM105",
|
|
119
|
+
"TRY300",
|
|
120
|
+
"TC001",
|
|
121
|
+
"TC003",
|
|
122
|
+
]
|
|
@@ -13,10 +13,13 @@ Supported formats
|
|
|
13
13
|
Public API
|
|
14
14
|
----------
|
|
15
15
|
from pyopenvba import (
|
|
16
|
-
ExcelFile, WordFile, PowerPointFile,
|
|
16
|
+
ExcelFile, WordFile, PowerPointFile, AccessReader,
|
|
17
17
|
pull, push, pull_word, push_word, pull_ppt, push_ppt,
|
|
18
|
+
pull_access,
|
|
18
19
|
)
|
|
19
20
|
|
|
21
|
+
# Access (.accdb) is READ-ONLY. See docs/msaccess_lessons_learned.md.
|
|
22
|
+
|
|
20
23
|
# In-process module edit (Excel)
|
|
21
24
|
with ExcelFile("workbook.xlsm") as wb:
|
|
22
25
|
modules = wb.vba_modules() # dict[name -> source]
|
|
@@ -44,26 +47,29 @@ Public API
|
|
|
44
47
|
|
|
45
48
|
pull_ppt("presentation.pptm", "./vba_src") # extract modules (PowerPoint)
|
|
46
49
|
push_ppt("./vba_src", "presentation.pptm")
|
|
50
|
+
|
|
51
|
+
pull_access("database.accdb", "./vba_src") # extract modules (Access, read-only)
|
|
47
52
|
"""
|
|
48
53
|
|
|
49
54
|
from pathlib import Path
|
|
50
|
-
from typing import Union
|
|
51
55
|
|
|
56
|
+
from pyopenvba.access_read import AccessReader
|
|
52
57
|
from pyopenvba.excel import ExcelFile
|
|
53
|
-
from pyopenvba.word import WordFile
|
|
54
|
-
from pyopenvba.powerpoint import PowerPointFile
|
|
55
58
|
from pyopenvba.exceptions import (
|
|
56
|
-
PyOpenVBAError,
|
|
57
59
|
CFBError,
|
|
58
|
-
|
|
60
|
+
PyOpenVBAError,
|
|
59
61
|
UnsupportedFormatError,
|
|
62
|
+
VBAProjectError,
|
|
60
63
|
)
|
|
61
|
-
from pyopenvba.
|
|
64
|
+
from pyopenvba.powerpoint import PowerPointFile
|
|
65
|
+
from pyopenvba.vba import VBAModuleKind
|
|
66
|
+
from pyopenvba.vba import synthesize_class_header as synthesize_class_header
|
|
67
|
+
from pyopenvba.word import WordFile
|
|
62
68
|
|
|
63
69
|
|
|
64
70
|
def pull(
|
|
65
|
-
workbook:
|
|
66
|
-
dest_dir:
|
|
71
|
+
workbook: str | Path,
|
|
72
|
+
dest_dir: str | Path,
|
|
67
73
|
*,
|
|
68
74
|
encoding: str = "utf-8",
|
|
69
75
|
overwrite: bool = True,
|
|
@@ -77,10 +83,10 @@ def pull(
|
|
|
77
83
|
|
|
78
84
|
|
|
79
85
|
def push(
|
|
80
|
-
src_dir:
|
|
81
|
-
workbook:
|
|
86
|
+
src_dir: str | Path,
|
|
87
|
+
workbook: str | Path,
|
|
82
88
|
*,
|
|
83
|
-
out:
|
|
89
|
+
out: str | Path | None = None,
|
|
84
90
|
encoding: str = "utf-8",
|
|
85
91
|
strict: bool = False,
|
|
86
92
|
) -> list[str]:
|
|
@@ -96,8 +102,8 @@ def push(
|
|
|
96
102
|
|
|
97
103
|
|
|
98
104
|
def pull_word(
|
|
99
|
-
document:
|
|
100
|
-
dest_dir:
|
|
105
|
+
document: str | Path,
|
|
106
|
+
dest_dir: str | Path,
|
|
101
107
|
*,
|
|
102
108
|
encoding: str = "utf-8",
|
|
103
109
|
overwrite: bool = True,
|
|
@@ -111,10 +117,10 @@ def pull_word(
|
|
|
111
117
|
|
|
112
118
|
|
|
113
119
|
def push_word(
|
|
114
|
-
src_dir:
|
|
115
|
-
document:
|
|
120
|
+
src_dir: str | Path,
|
|
121
|
+
document: str | Path,
|
|
116
122
|
*,
|
|
117
|
-
out:
|
|
123
|
+
out: str | Path | None = None,
|
|
118
124
|
encoding: str = "utf-8",
|
|
119
125
|
strict: bool = False,
|
|
120
126
|
) -> list[str]:
|
|
@@ -130,8 +136,8 @@ def push_word(
|
|
|
130
136
|
|
|
131
137
|
|
|
132
138
|
def pull_ppt(
|
|
133
|
-
presentation:
|
|
134
|
-
dest_dir:
|
|
139
|
+
presentation: str | Path,
|
|
140
|
+
dest_dir: str | Path,
|
|
135
141
|
*,
|
|
136
142
|
encoding: str = "utf-8",
|
|
137
143
|
overwrite: bool = True,
|
|
@@ -145,10 +151,10 @@ def pull_ppt(
|
|
|
145
151
|
|
|
146
152
|
|
|
147
153
|
def push_ppt(
|
|
148
|
-
src_dir:
|
|
149
|
-
presentation:
|
|
154
|
+
src_dir: str | Path,
|
|
155
|
+
presentation: str | Path,
|
|
150
156
|
*,
|
|
151
|
-
out:
|
|
157
|
+
out: str | Path | None = None,
|
|
152
158
|
encoding: str = "utf-8",
|
|
153
159
|
strict: bool = False,
|
|
154
160
|
) -> list[str]:
|
|
@@ -163,21 +169,39 @@ def push_ppt(
|
|
|
163
169
|
return updated
|
|
164
170
|
|
|
165
171
|
|
|
172
|
+
def pull_access(
|
|
173
|
+
database: str | Path,
|
|
174
|
+
dest_dir: str | Path,
|
|
175
|
+
*,
|
|
176
|
+
encoding: str = "utf-8",
|
|
177
|
+
overwrite: bool = True,
|
|
178
|
+
) -> list[Path]:
|
|
179
|
+
"""
|
|
180
|
+
Export every VBA module from an Access ``database`` (.accdb) into
|
|
181
|
+
``dest_dir`` as ``.bas`` / ``.cls`` files. Returns the paths
|
|
182
|
+
written. Mirrors :func:`pull` / :func:`pull_word` / :func:`pull_ppt`.
|
|
183
|
+
"""
|
|
184
|
+
db = AccessReader(database)
|
|
185
|
+
return db.pull_modules(dest_dir, encoding=encoding, overwrite=overwrite)
|
|
186
|
+
|
|
187
|
+
|
|
166
188
|
__all__ = [
|
|
189
|
+
"AccessReader",
|
|
190
|
+
"CFBError",
|
|
167
191
|
"ExcelFile",
|
|
168
|
-
"WordFile",
|
|
169
192
|
"PowerPointFile",
|
|
170
|
-
"VBAModuleKind",
|
|
171
193
|
"PyOpenVBAError",
|
|
172
|
-
"CFBError",
|
|
173
|
-
"VBAProjectError",
|
|
174
194
|
"UnsupportedFormatError",
|
|
195
|
+
"VBAModuleKind",
|
|
196
|
+
"VBAProjectError",
|
|
197
|
+
"WordFile",
|
|
175
198
|
"pull",
|
|
176
|
-
"
|
|
177
|
-
"pull_word",
|
|
178
|
-
"push_word",
|
|
199
|
+
"pull_access",
|
|
179
200
|
"pull_ppt",
|
|
201
|
+
"pull_word",
|
|
202
|
+
"push",
|
|
180
203
|
"push_ppt",
|
|
204
|
+
"push_word",
|
|
181
205
|
]
|
|
182
206
|
|
|
183
207
|
__version__ = "2.0.0"
|