Split out of #27, tier 2 of three. Depends on #127 for reaching the bytes.
Reading what the macros in a document actually are — module names, their source, whether the project is locked or signed. The auditing case ("what does this document run") and the review case ("diff the macros between two versions of our template") both need it, and neither is served by opaque bytes.
Dependency: olefile, not oletools
The issue body proposes oletools/olevba. That brings pyparsing, colorclass, pcodedmp, msoffcrypto-tool and easygui — a Tkinter GUI toolkit — into the dependency tree of a document library whose runtime deps are lxml and typing_extensions. Not acceptable as a hard requirement, and olevba is oriented at malware triage rather than at being a library.
olefile is the right piece: BSD, pure Python, zero dependencies, actively maintained, and it is exactly the OLE compound-file reader this needs. It is also what oletools itself uses underneath.
Behind an optional extra:
[project.optional-dependencies]
vba = ["olefile>=0.47"]
so the default install is unchanged and document.vba_project.modules raises a clear "install python-docx-ng[vba]" message when the extra is absent.
Shape
>>> project = document.vba_project # richer object than tier 1's bytes
>>> [m.name for m in project.modules]
['ThisDocument', 'Module1', 'UserForm1']
>>> project.modules['Module1'].code
'Sub Hello()\n MsgBox "hi"\nEnd Sub\n'
>>> project.is_locked, project.is_signed
(False, True)
Keep Document.vba_project returning the tier-1 bytes accessor's object and hang the parsed view off it, or the two tiers fight over the same name. Deciding that boundary is part of this issue.
The work beyond olefile
olefile gets you the storage/stream tree. The module source is compressed with the VBA run-length scheme, which olefile does not implement. That algorithm is [MS-OVBA] §2.4.1 "Compression and Decompression" — a public Microsoft specification, roughly 100 lines to implement, and the single trickiest part of this tier. It is a chunk-based scheme with a 4096-byte window and a variable-width copy token; the bit-width of the token changes with the current decompressed position, which is where naive implementations go wrong.
Locating the module streams needs the dir stream (itself compressed) and the PROJECT stream, which names the modules and their types.
Blocker: no usable fixture
tests/test_files/macro-enabled.docm has a 27-byte stub vbaProject.bin, not a real OLE file. This tier cannot be tested without a genuine macro project, which means authoring one in Word or sourcing one — the same blocker as #23. Worth resolving before starting rather than discovering halfway.
A real fixture should be deliberately trivial (one module, one Sub, no forms, no signature) so it is reviewable, and its provenance recorded in the test file.
Scope note
Read-only. Editing a module's source means rewriting the compound file, which is #129.
Split out of #27, tier 2 of three. Depends on #127 for reaching the bytes.
Reading what the macros in a document actually are — module names, their source, whether the project is locked or signed. The auditing case ("what does this document run") and the review case ("diff the macros between two versions of our template") both need it, and neither is served by opaque bytes.
Dependency:
olefile, notoletoolsThe issue body proposes
oletools/olevba. That bringspyparsing,colorclass,pcodedmp,msoffcrypto-toolandeasygui— a Tkinter GUI toolkit — into the dependency tree of a document library whose runtime deps arelxmlandtyping_extensions. Not acceptable as a hard requirement, andolevbais oriented at malware triage rather than at being a library.olefileis the right piece: BSD, pure Python, zero dependencies, actively maintained, and it is exactly the OLE compound-file reader this needs. It is also whatoletoolsitself uses underneath.Behind an optional extra:
so the default install is unchanged and
document.vba_project.modulesraises a clear "install python-docx-ng[vba]" message when the extra is absent.Shape
Keep
Document.vba_projectreturning the tier-1 bytes accessor's object and hang the parsed view off it, or the two tiers fight over the same name. Deciding that boundary is part of this issue.The work beyond
olefileolefilegets you the storage/stream tree. The module source is compressed with the VBA run-length scheme, whicholefiledoes not implement. That algorithm is [MS-OVBA] §2.4.1 "Compression and Decompression" — a public Microsoft specification, roughly 100 lines to implement, and the single trickiest part of this tier. It is a chunk-based scheme with a 4096-byte window and a variable-width copy token; the bit-width of the token changes with the current decompressed position, which is where naive implementations go wrong.Locating the module streams needs the
dirstream (itself compressed) and thePROJECTstream, which names the modules and their types.Blocker: no usable fixture
tests/test_files/macro-enabled.docmhas a 27-byte stubvbaProject.bin, not a real OLE file. This tier cannot be tested without a genuine macro project, which means authoring one in Word or sourcing one — the same blocker as #23. Worth resolving before starting rather than discovering halfway.A real fixture should be deliberately trivial (one module, one
Sub, no forms, no signature) so it is reviewable, and its provenance recorded in the test file.Scope note
Read-only. Editing a module's source means rewriting the compound file, which is #129.