pyOpenVBA 2.0.0__tar.gz → 3.0.0__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.
Files changed (26) hide show
  1. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/.gitignore +5 -0
  2. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/PKG-INFO +50 -18
  3. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/README.md +49 -17
  4. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/docs/architecture.md +108 -1
  5. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/pyproject.toml +35 -1
  6. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/__init__.py +52 -28
  7. pyopenvba-3.0.0/src/pyopenvba/__main__.py +216 -0
  8. pyopenvba-3.0.0/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  9. pyopenvba-3.0.0/src/pyopenvba/access_read.py +1728 -0
  10. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/cfb.py +11 -10
  11. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/excel.py +10 -9
  12. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/powerpoint.py +10 -9
  13. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/vba.py +92 -26
  14. pyopenvba-3.0.0/src/pyopenvba/vba_pcode.py +736 -0
  15. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/word.py +10 -9
  16. pyopenvba-2.0.0/src/pyopenvba/__main__.py +0 -68
  17. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/LICENSE.md +0 -0
  18. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  19. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/docs/roadmap.md +0 -0
  20. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/__init__.py +0 -0
  21. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  22. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  23. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  24. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  25. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/exceptions.py +0 -0
  26. {pyopenvba-2.0.0 → pyopenvba-3.0.0}/tests/fuzz_corpus/README.md +0 -0
@@ -50,3 +50,8 @@ demo/output/
50
50
  # Debug/diagnostic scratch scripts
51
51
  demo/_diag*.py
52
52
  demo/_isolate.py
53
+
54
+ # Access p-code RE corpus (regenerable via
55
+ # tests/live_access_test/_corpus_generate.ps1)
56
+ tests/live_access_test/re_corpus/
57
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyOpenVBA
3
- Version: 2.0.0
3
+ Version: 3.0.0
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
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
39
39
  [![PyPI version](https://img.shields.io/pypi/v/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
40
40
  [![Python versions](https://img.shields.io/pypi/pyversions/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
41
41
  [![CI](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml/badge.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
42
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md)
42
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
43
43
  [![Downloads](https://img.shields.io/pypi/dm/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
44
44
 
45
45
  **Read and write VBA macros inside Excel, Word, and PowerPoint files, in pure Python.**
@@ -52,6 +52,8 @@ 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**, see
56
+ [docs/msaccess_lessons_learned.md](docs/msaccess_lessons_learned.md)
55
57
 
56
58
  <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
59
 
@@ -226,18 +228,9 @@ with ExcelFile("workbook.xlsm") as wb:
226
228
  kind=VBAModuleKind.standard,
227
229
  )
228
230
 
229
- # Add a class module (VB_Base attribute is required)
230
- _CLASS_VB_BASE = "0{FCFB3D2A-A0FA-1068-A738-08002B3371B5}"
231
+ # Add a class module (header is synthesized automatically)
231
232
  project.add_module(
232
233
  "MyClass",
233
- f'Attribute VB_Name = "MyClass"\r\n'
234
- f'Attribute VB_Base = "{_CLASS_VB_BASE}"\r\n'
235
- "Attribute VB_GlobalNameSpace = False\r\n"
236
- "Attribute VB_Creatable = False\r\n"
237
- "Attribute VB_PredeclaredId = False\r\n"
238
- "Attribute VB_Exposed = False\r\n"
239
- "Attribute VB_TemplateDerived = False\r\n"
240
- "Attribute VB_Customizable = False\r\n"
241
234
  "Option Explicit\r\n",
242
235
  kind=VBAModuleKind.other,
243
236
  )
@@ -335,6 +328,45 @@ modules, `.cls` for class modules and code-behind.
335
328
  | `.potm` | Macro-enabled template | yes | yes | no |
336
329
  | `.ppt` | Legacy (PowerPoint 97-2003) | yes | yes | no |
337
330
 
331
+ ### Access (read-only)
332
+
333
+ | Extension | What it is | Read | Write | create_new |
334
+ |-----------|------------------------------|:----:|:-----:|:----------:|
335
+ | `.accdb` | Access database (ACE engine) | yes | no | no |
336
+
337
+ Access stores compiled VBA p-code (the `rU@` + `CAFE` rows in the LVAL
338
+ catalog) separately from the OVBA source cache. The compiled p-code is
339
+ authoritative for the Access GUI; mutations to the source cache do not
340
+ survive reload because Access never recompiles from the cache. After
341
+ extensive reverse-engineering experiments we concluded that a
342
+ production-quality writer would require a complete VBA7 p-code
343
+ assembler, which is out of scope. See
344
+ [docs/msaccess_lessons_learned.md](docs/msaccess_lessons_learned.md)
345
+ for the full chronicle.
346
+
347
+ What `AccessReader` does support:
348
+
349
+ - `AccessReader(path)` / `vba_module_names()` / `read_vba_module(name)`
350
+ - `read_vba_module_with_attributes(name)`
351
+ - `vba_modules()` (dict of name -> source)
352
+ - `iter_vba_modules()` (rich `VBAModule` records)
353
+ - `export_module()` / `export_modules()` / `pull_modules()` (write `.bas` / `.cls` to disk)
354
+ - `read_project_info()`, `identifiers()`, `find_interned_strings()`,
355
+ `find_module_streams()`, `iter_pcode_streams()`, `disassemble_module()`
356
+ - `iter_msys_objects()` / `msys_objects()` / `iter_msys_modules()` /
357
+ `find_msys_module()` (MSysObjects catalog inspection)
358
+ - Top-level helper: `pyopenvba.pull_access(database, dest_dir)`
359
+
360
+ ```python
361
+ from pyopenvba import AccessReader, pull_access
362
+
363
+ with AccessReader("database.accdb") as db:
364
+ for name, source in db.vba_modules().items():
365
+ print(name, len(source))
366
+
367
+ pull_access("database.accdb", "./vba_src") # export every module to .bas / .cls
368
+ ```
369
+
338
370
  Every save is verified to reopen in the host application **without** the
339
371
  "we found a problem with some content" repair dialog.
340
372
 
@@ -385,7 +417,7 @@ following are preserved byte-for-byte but not interpreted:
385
417
  - Re-signing digitally signed projects.
386
418
  - ActiveX license editing.
387
419
 
388
- See [docs/roadmap.md](docs/roadmap.md) for the full feature matrix.
420
+ See [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md) for the full feature matrix.
389
421
 
390
422
  ---
391
423
 
@@ -395,7 +427,7 @@ See [docs/roadmap.md](docs/roadmap.md) for the full feature matrix.
395
427
  src/pyopenvba/
396
428
  __init__.py public API (ExcelFile, WordFile, PowerPointFile,
397
429
  pull/push, pull_word/push_word, pull_ppt/push_ppt,
398
- VBAModuleKind, exceptions)
430
+ VBAModuleKind, synthesize_class_header, exceptions)
399
431
  excel.py ExcelFile facade (ZIP / CFB dispatch, pull/push helpers)
400
432
  word.py WordFile facade
401
433
  powerpoint.py PowerPointFile facade
@@ -408,10 +440,10 @@ src/pyopenvba/
408
440
 
409
441
  For deeper documentation:
410
442
 
411
- - [docs/architecture.md](docs/architecture.md) - internal module layout.
412
- - [docs/ms-ovba-implementation-guide_v2.md](docs/ms-ovba-implementation-guide_v2.md) -
443
+ - [docs/architecture.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/architecture.md) - internal module layout.
444
+ - [docs/ms-ovba-implementation-guide_v2.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/ms-ovba-implementation-guide_v2.md) -
413
445
  language-agnostic guide for re-implementing MS-OVBA in another language.
414
- - [docs/roadmap.md](docs/roadmap.md) - per-feature implementation status.
446
+ - [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md) - per-feature implementation status.
415
447
 
416
448
  ---
417
449
 
@@ -438,4 +470,4 @@ pushed.
438
470
 
439
471
  ## License
440
472
 
441
- [MIT](LICENSE.md).
473
+ [MIT](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md).
@@ -3,7 +3,7 @@
3
3
  [![PyPI version](https://img.shields.io/pypi/v/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
4
4
  [![Python versions](https://img.shields.io/pypi/pyversions/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
5
5
  [![CI](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml/badge.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
6
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
7
7
  [![Downloads](https://img.shields.io/pypi/dm/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
8
8
 
9
9
  **Read and write VBA macros inside Excel, Word, and PowerPoint files, in pure Python.**
@@ -16,6 +16,8 @@ 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**, see
20
+ [docs/msaccess_lessons_learned.md](docs/msaccess_lessons_learned.md)
19
21
 
20
22
  <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
23
 
@@ -190,18 +192,9 @@ with ExcelFile("workbook.xlsm") as wb:
190
192
  kind=VBAModuleKind.standard,
191
193
  )
192
194
 
193
- # Add a class module (VB_Base attribute is required)
194
- _CLASS_VB_BASE = "0{FCFB3D2A-A0FA-1068-A738-08002B3371B5}"
195
+ # Add a class module (header is synthesized automatically)
195
196
  project.add_module(
196
197
  "MyClass",
197
- f'Attribute VB_Name = "MyClass"\r\n'
198
- f'Attribute VB_Base = "{_CLASS_VB_BASE}"\r\n'
199
- "Attribute VB_GlobalNameSpace = False\r\n"
200
- "Attribute VB_Creatable = False\r\n"
201
- "Attribute VB_PredeclaredId = False\r\n"
202
- "Attribute VB_Exposed = False\r\n"
203
- "Attribute VB_TemplateDerived = False\r\n"
204
- "Attribute VB_Customizable = False\r\n"
205
198
  "Option Explicit\r\n",
206
199
  kind=VBAModuleKind.other,
207
200
  )
@@ -299,6 +292,45 @@ modules, `.cls` for class modules and code-behind.
299
292
  | `.potm` | Macro-enabled template | yes | yes | no |
300
293
  | `.ppt` | Legacy (PowerPoint 97-2003) | yes | yes | no |
301
294
 
295
+ ### Access (read-only)
296
+
297
+ | Extension | What it is | Read | Write | create_new |
298
+ |-----------|------------------------------|:----:|:-----:|:----------:|
299
+ | `.accdb` | Access database (ACE engine) | yes | no | no |
300
+
301
+ Access stores compiled VBA p-code (the `rU@` + `CAFE` rows in the LVAL
302
+ catalog) separately from the OVBA source cache. The compiled p-code is
303
+ authoritative for the Access GUI; mutations to the source cache do not
304
+ survive reload because Access never recompiles from the cache. After
305
+ extensive reverse-engineering experiments we concluded that a
306
+ production-quality writer would require a complete VBA7 p-code
307
+ assembler, which is out of scope. See
308
+ [docs/msaccess_lessons_learned.md](docs/msaccess_lessons_learned.md)
309
+ for the full chronicle.
310
+
311
+ What `AccessReader` does support:
312
+
313
+ - `AccessReader(path)` / `vba_module_names()` / `read_vba_module(name)`
314
+ - `read_vba_module_with_attributes(name)`
315
+ - `vba_modules()` (dict of name -> source)
316
+ - `iter_vba_modules()` (rich `VBAModule` records)
317
+ - `export_module()` / `export_modules()` / `pull_modules()` (write `.bas` / `.cls` to disk)
318
+ - `read_project_info()`, `identifiers()`, `find_interned_strings()`,
319
+ `find_module_streams()`, `iter_pcode_streams()`, `disassemble_module()`
320
+ - `iter_msys_objects()` / `msys_objects()` / `iter_msys_modules()` /
321
+ `find_msys_module()` (MSysObjects catalog inspection)
322
+ - Top-level helper: `pyopenvba.pull_access(database, dest_dir)`
323
+
324
+ ```python
325
+ from pyopenvba import AccessReader, pull_access
326
+
327
+ with AccessReader("database.accdb") as db:
328
+ for name, source in db.vba_modules().items():
329
+ print(name, len(source))
330
+
331
+ pull_access("database.accdb", "./vba_src") # export every module to .bas / .cls
332
+ ```
333
+
302
334
  Every save is verified to reopen in the host application **without** the
303
335
  "we found a problem with some content" repair dialog.
304
336
 
@@ -349,7 +381,7 @@ following are preserved byte-for-byte but not interpreted:
349
381
  - Re-signing digitally signed projects.
350
382
  - ActiveX license editing.
351
383
 
352
- See [docs/roadmap.md](docs/roadmap.md) for the full feature matrix.
384
+ See [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md) for the full feature matrix.
353
385
 
354
386
  ---
355
387
 
@@ -359,7 +391,7 @@ See [docs/roadmap.md](docs/roadmap.md) for the full feature matrix.
359
391
  src/pyopenvba/
360
392
  __init__.py public API (ExcelFile, WordFile, PowerPointFile,
361
393
  pull/push, pull_word/push_word, pull_ppt/push_ppt,
362
- VBAModuleKind, exceptions)
394
+ VBAModuleKind, synthesize_class_header, exceptions)
363
395
  excel.py ExcelFile facade (ZIP / CFB dispatch, pull/push helpers)
364
396
  word.py WordFile facade
365
397
  powerpoint.py PowerPointFile facade
@@ -372,10 +404,10 @@ src/pyopenvba/
372
404
 
373
405
  For deeper documentation:
374
406
 
375
- - [docs/architecture.md](docs/architecture.md) - internal module layout.
376
- - [docs/ms-ovba-implementation-guide_v2.md](docs/ms-ovba-implementation-guide_v2.md) -
407
+ - [docs/architecture.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/architecture.md) - internal module layout.
408
+ - [docs/ms-ovba-implementation-guide_v2.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/ms-ovba-implementation-guide_v2.md) -
377
409
  language-agnostic guide for re-implementing MS-OVBA in another language.
378
- - [docs/roadmap.md](docs/roadmap.md) - per-feature implementation status.
410
+ - [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md) - per-feature implementation status.
379
411
 
380
412
  ---
381
413
 
@@ -402,4 +434,4 @@ pushed.
402
434
 
403
435
  ## License
404
436
 
405
- [MIT](LICENSE.md).
437
+ [MIT](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md).
@@ -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 {pull,push,ls}` CLI.
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 = "2.0.0"
7
+ version = "3.0.0"
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
- VBAProjectError,
60
+ PyOpenVBAError,
59
61
  UnsupportedFormatError,
62
+ VBAProjectError,
60
63
  )
64
+ from pyopenvba.powerpoint import PowerPointFile
61
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: Union[str, Path],
66
- dest_dir: Union[str, Path],
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: Union[str, Path],
81
- workbook: Union[str, Path],
86
+ src_dir: str | Path,
87
+ workbook: str | Path,
82
88
  *,
83
- out: Union[str, Path, None] = None,
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: Union[str, Path],
100
- dest_dir: Union[str, Path],
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: Union[str, Path],
115
- document: Union[str, Path],
120
+ src_dir: str | Path,
121
+ document: str | Path,
116
122
  *,
117
- out: Union[str, Path, None] = None,
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: Union[str, Path],
134
- dest_dir: Union[str, Path],
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: Union[str, Path],
149
- presentation: Union[str, Path],
154
+ src_dir: str | Path,
155
+ presentation: str | Path,
150
156
  *,
151
- out: Union[str, Path, None] = None,
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
- "push",
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"