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.
Files changed (26) hide show
  1. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/.gitignore +5 -0
  2. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/PKG-INFO +62 -5
  3. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/README.md +61 -4
  4. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/docs/architecture.md +108 -1
  5. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/pyproject.toml +35 -1
  6. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/__init__.py +53 -29
  7. pyopenvba-3.0.1/src/pyopenvba/__main__.py +216 -0
  8. pyopenvba-3.0.1/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  9. pyopenvba-3.0.1/src/pyopenvba/access_read.py +1728 -0
  10. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/cfb.py +11 -10
  11. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/excel.py +10 -9
  12. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/powerpoint.py +10 -9
  13. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/vba.py +62 -25
  14. pyopenvba-3.0.1/src/pyopenvba/vba_pcode.py +736 -0
  15. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/word.py +10 -9
  16. pyopenvba-2.0.1/src/pyopenvba/__main__.py +0 -68
  17. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/LICENSE.md +0 -0
  18. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  19. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/docs/roadmap.md +0 -0
  20. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/__init__.py +0 -0
  21. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  22. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  23. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  24. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  25. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/src/pyopenvba/exceptions.py +0 -0
  26. {pyopenvba-2.0.1 → pyopenvba-3.0.1}/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.1
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
  [![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
42
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
43
- [![Downloads](https://img.shields.io/pypi/dm/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
43
+ [![Downloads](https://static.pepy.tech/badge/pyOpenVBA/month)](https://pepy.tech/project/pyOpenVBA)
44
44
 
45
- **Read and write VBA macros inside Excel, Word, and PowerPoint files, in pure Python.**
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
- The API is identical across all three hosts: `module_names()`, `get_module()`,
174
- `set_module()`, `save()`.
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
  [![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
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
7
- [![Downloads](https://img.shields.io/pypi/dm/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
7
+ [![Downloads](https://static.pepy.tech/badge/pyOpenVBA/month)](https://pepy.tech/project/pyOpenVBA)
8
8
 
9
- **Read and write VBA macros inside Excel, Word, and PowerPoint files, in pure Python.**
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
- The API is identical across all three hosts: `module_names()`, `get_module()`,
138
- `set_module()`, `save()`.
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 {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.1"
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
- VBAProjectError,
60
+ PyOpenVBAError,
59
61
  UnsupportedFormatError,
62
+ VBAProjectError,
60
63
  )
61
- from pyopenvba.vba import VBAModuleKind, synthesize_class_header
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: 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"