pyOpenVBA 3.5.0__tar.gz → 4.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 (78) hide show
  1. pyopenvba-4.0.0/PKG-INFO +571 -0
  2. pyopenvba-4.0.0/README.md +535 -0
  3. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/architecture.md +79 -106
  4. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/ms-ovba-implementation-guide_v2.md +4 -4
  5. pyopenvba-4.0.0/docs/research/access_complex/README.md +78 -0
  6. pyopenvba-4.0.0/docs/research/access_designs/README.md +109 -0
  7. pyopenvba-4.0.0/docs/research/access_macros/README.md +135 -0
  8. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/research/access_write/README.md +455 -55
  9. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/roadmap.md +19 -11
  10. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/pyproject.toml +5 -2
  11. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/__init__.py +35 -5
  12. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/__main__.py +22 -0
  13. pyopenvba-4.0.0/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
  14. pyopenvba-4.0.0/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
  15. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.blob +0 -0
  16. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.lvprop +0 -0
  17. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.propdata +0 -0
  18. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.prototypes +0 -0
  19. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
  20. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.blob +0 -0
  21. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.lvprop +0 -0
  22. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.propdata +0 -0
  23. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.prototypes +0 -0
  24. pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
  25. pyopenvba-4.0.0/src/pyopenvba/access/__init__.py +22 -0
  26. pyopenvba-4.0.0/src/pyopenvba/access/_alloc.py +325 -0
  27. pyopenvba-4.0.0/src/pyopenvba/access/_btree.py +429 -0
  28. pyopenvba-4.0.0/src/pyopenvba/access/_collation.py +234 -0
  29. pyopenvba-4.0.0/src/pyopenvba/access/_collation_general_legacy.py +5698 -0
  30. pyopenvba-4.0.0/src/pyopenvba/access/_compact.py +567 -0
  31. pyopenvba-4.0.0/src/pyopenvba/access/_complex.py +230 -0
  32. pyopenvba-4.0.0/src/pyopenvba/access/_datapage.py +204 -0
  33. pyopenvba-4.0.0/src/pyopenvba/access/_ddl.py +380 -0
  34. pyopenvba-4.0.0/src/pyopenvba/access/_deflate.py +667 -0
  35. pyopenvba-4.0.0/src/pyopenvba/access/_designs.py +2905 -0
  36. pyopenvba-4.0.0/src/pyopenvba/access/_facade.py +362 -0
  37. pyopenvba-4.0.0/src/pyopenvba/access/_format.py +282 -0
  38. pyopenvba-4.0.0/src/pyopenvba/access/_index.py +514 -0
  39. pyopenvba-4.0.0/src/pyopenvba/access/_lval.py +242 -0
  40. pyopenvba-4.0.0/src/pyopenvba/access/_macros.py +161 -0
  41. pyopenvba-4.0.0/src/pyopenvba/access/_pages.py +548 -0
  42. pyopenvba-4.0.0/src/pyopenvba/access/_props.py +278 -0
  43. pyopenvba-4.0.0/src/pyopenvba/access/_queries.py +660 -0
  44. pyopenvba-4.0.0/src/pyopenvba/access/_rows.py +466 -0
  45. pyopenvba-4.0.0/src/pyopenvba/access/_schema.py +670 -0
  46. pyopenvba-4.0.0/src/pyopenvba/access/_sql.py +3283 -0
  47. pyopenvba-4.0.0/src/pyopenvba/access/_storage.py +119 -0
  48. pyopenvba-4.0.0/src/pyopenvba/access/_tdef.py +497 -0
  49. pyopenvba-4.0.0/src/pyopenvba/access/_validate.py +171 -0
  50. pyopenvba-4.0.0/src/pyopenvba/access/_vba.py +573 -0
  51. pyopenvba-4.0.0/src/pyopenvba/access/database.py +4331 -0
  52. pyopenvba-3.5.0/PKG-INFO +0 -633
  53. pyopenvba-3.5.0/README.md +0 -597
  54. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/.gitignore +0 -0
  55. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/LICENSE.md +0 -0
  56. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/research/pcode/README.md +0 -0
  57. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_host.py +0 -0
  58. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_oforms_pages.py +0 -0
  59. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_oforms_records.py +0 -0
  60. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_ppt_container.py +0 -0
  61. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/__init__.py +0 -0
  62. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  63. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
  64. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  65. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  66. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  67. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  68. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  69. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/access_read.py +0 -0
  70. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/cfb.py +0 -0
  71. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/excel.py +0 -0
  72. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/exceptions.py +0 -0
  73. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/forms.py +0 -0
  74. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/powerpoint.py +0 -0
  75. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/vba.py +0 -0
  76. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/vba_pcode.py +0 -0
  77. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/word.py +0 -0
  78. {pyopenvba-3.5.0 → pyopenvba-4.0.0}/tests/fuzz_corpus/README.md +0 -0
@@ -0,0 +1,571 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyOpenVBA
3
+ Version: 4.0.0
4
+ Summary: Read and write VBA macros inside Excel, Word, PowerPoint and Access files in pure Python, no dependencies.
5
+ Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyOpenVBA
6
+ Project-URL: Repository, https://github.com/WilliamSmithEdward/pyOpenVBA
7
+ Project-URL: Issues, https://github.com/WilliamSmithEdward/pyOpenVBA/issues
8
+ Project-URL: Documentation, https://github.com/WilliamSmithEdward/pyOpenVBA#readme
9
+ Author-email: William Smith <williamsmithe@icloud.com>
10
+ License: MIT
11
+ License-File: LICENSE.md
12
+ Keywords: accdb,access,docm,excel,macros,mdb,ms-cfb,ms-ovba,office,powerpoint,pptm,vba,word,xlam,xls,xlsb,xlsm
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Office/Business
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Topic :: System :: Archiving
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Provides-Extra: dev
31
+ Requires-Dist: build>=1.2; extra == 'dev'
32
+ Requires-Dist: pyright>=1.1.350; extra == 'dev'
33
+ Requires-Dist: pytest>=8; extra == 'dev'
34
+ Requires-Dist: twine>=5; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # pyOpenVBA
38
+
39
+ [![PyPI version](https://img.shields.io/pypi/v/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
40
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
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)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
43
+ [![Downloads](https://static.pepy.tech/badge/pyOpenVBA/month)](https://pepy.tech/project/pyOpenVBA)
44
+
45
+ **Read and write VBA macros inside Office files, in pure Python.**
46
+
47
+ No dependencies beyond the standard library. No Office install needed.
48
+ Works on Windows, macOS and Linux. Python 3.10 or newer.
49
+
50
+ Four hosts, one API:
51
+
52
+ * Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`)
53
+ * Word (`.docm`, `.dotm`, `.doc`)
54
+ * PowerPoint (`.pptm`, `.potm`, `.ppt`)
55
+ * Access (`.accdb`, `.mdb`)
56
+
57
+ ---
58
+
59
+ ## Why use this?
60
+
61
+ Good Python tools exist for reading VBA out of Office files (oletools,
62
+ olefile and friends), and they remain the right choice for forensics,
63
+ malware analysis and audits. pyOpenVBA covers the next step: writing
64
+ changes back so the file still opens cleanly in the host application.
65
+
66
+ The write path is the point of the library:
67
+
68
+ - **Modify** a module's source in place.
69
+ - **Add** a standard module, a class module, or code behind a form.
70
+ - **Rename** a module everywhere its name lives, in one step.
71
+ - **Delete** a module cleanly.
72
+ - **Design** forms as well as their code: read a form's controls and
73
+ properties, edit them, add and remove controls, or build a form from
74
+ nothing.
75
+ - **Create** a new `.xlsm`, `.xlsb`, `.xlam`, `.docm`, `.pptm` or
76
+ `.accdb` file and put code in it.
77
+ - **Save**, and have the file reopen in the host with no repair dialog.
78
+
79
+ Every format is verified against live Office: the saved file reopens
80
+ without a repair prompt, and the code in it runs. Access edits are held
81
+ to a stricter bar. Each write is compared byte for byte with the same
82
+ edit made by Access or its database engine.
83
+
84
+ That makes it a good fit for:
85
+
86
+ - Version-controlling VBA in git like any other source, then pushing
87
+ edits back without opening Office.
88
+ - Diffing two files to see what changed in a module or a form's design.
89
+ - Building forms and macros from a script on a machine without Office.
90
+ - Reading and writing macros on a server or in CI.
91
+ - Letting an AI agent read and change the code in your Office files.
92
+
93
+ ---
94
+
95
+ ## Installation
96
+
97
+ ```bash
98
+ pip install pyOpenVBA
99
+ ```
100
+
101
+ Requires Python 3.10 or newer. There are no other dependencies.
102
+
103
+ After installing, the CLI is available as a module or as a script:
104
+
105
+ ```bash
106
+ python -m pyopenvba --help
107
+ pyopenvba --help
108
+ ```
109
+
110
+ From source, for development:
111
+
112
+ ```bash
113
+ git clone https://github.com/WilliamSmithEdward/pyOpenVBA
114
+ cd pyOpenVBA
115
+ pip install -e ".[dev]"
116
+ ```
117
+
118
+ ---
119
+
120
+ ## 30-second tour
121
+
122
+ The four host classes share the same module API: `module_names()`,
123
+ `get_module()`, `set_module()`, `save()`.
124
+
125
+ ### Excel
126
+
127
+ ```python
128
+ from pyopenvba import ExcelFile
129
+
130
+ with ExcelFile("workbook.xlsm") as wb:
131
+ print(wb.module_names()) # ['ThisWorkbook', 'Sheet1', 'Module1']
132
+ source = wb.get_module("Module1")
133
+ wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "hi"\r\nEnd Sub\r\n')
134
+ wb.save() # in place
135
+ # wb.save("edited.xlsm") # or to a new file
136
+ ```
137
+
138
+ ### Word
139
+
140
+ ```python
141
+ from pyopenvba import WordFile
142
+
143
+ with WordFile("document.docm") as doc:
144
+ print(doc.module_names()) # ['ThisDocument', 'Module1']
145
+ doc.set_module("Module1", 'Sub Hello()\r\n MsgBox "hi"\r\nEnd Sub\r\n')
146
+ doc.save()
147
+ ```
148
+
149
+ ### PowerPoint
150
+
151
+ ```python
152
+ from pyopenvba import PowerPointFile
153
+
154
+ with PowerPointFile("presentation.pptm") as prs:
155
+ print(prs.module_names()) # ['Module1']
156
+ prs.set_module("Module1", 'Sub Hello()\r\n MsgBox "hi"\r\nEnd Sub\r\n')
157
+ prs.save()
158
+ ```
159
+
160
+ ### Access
161
+
162
+ ```python
163
+ from pyopenvba import AccessDatabase
164
+
165
+ with AccessDatabase("database.accdb") as db:
166
+ print(db.module_names()) # ['Module1', 'Form_Orders']
167
+ source = db.get_module("Module1")
168
+ db.set_module("Module1", "Option Compare Database\r\n\r\nPublic Sub Hello()\r\n MsgBox \"hi\"\r\nEnd Sub")
169
+ db.save()
170
+ ```
171
+
172
+ Access keeps a compiled copy of the project beside the source and runs
173
+ that copy. Writing source through pyOpenVBA marks the project for
174
+ recompilation, so Access rebuilds it from the new source the next time it
175
+ opens the file. The code has to compile, and Access takes a moment on
176
+ that first open. Every one of these operations is checked by running the
177
+ result in Access and comparing the value the code returns.
178
+
179
+ ---
180
+
181
+ ## Create a new file
182
+
183
+ `create_new()` builds a fresh macro-enabled file from a template Office
184
+ authored itself, so it opens with no repair prompt. The extension picks
185
+ the format:
186
+
187
+ ```python
188
+ from pyopenvba import AccessDatabase, ExcelFile, PowerPointFile, WordFile
189
+
190
+ with ExcelFile.create_new("new_book.xlsm") as wb: # also .xlsb, .xlam
191
+ wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlsm"\r\nEnd Sub\r\n')
192
+ wb.save()
193
+
194
+ with WordFile.create_new("new_doc.docm") as doc:
195
+ doc.set_module("Module1", 'Sub Hello()\r\n MsgBox "docm"\r\nEnd Sub\r\n')
196
+ doc.save()
197
+
198
+ with PowerPointFile.create_new("new_prs.pptm") as prs:
199
+ prs.set_module("Module1", 'Sub Hello()\r\n MsgBox "pptm"\r\nEnd Sub\r\n')
200
+ prs.save()
201
+
202
+ with AccessDatabase.create_new("new_db.accdb") as db:
203
+ db.set_module("Module1", "Option Compare Database\r\n\r\nPublic Sub Hello()\r\nEnd Sub")
204
+ db.save()
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Add, rename or delete a module
210
+
211
+ `vba_project()` gives the project, and the same three calls work on every
212
+ host:
213
+
214
+ ```python
215
+ from pyopenvba import ExcelFile, VBAModuleKind
216
+
217
+ with ExcelFile("workbook.xlsm") as wb:
218
+ project = wb.vba_project()
219
+ project.add_module("NewModule", 'Sub Hi()\r\n MsgBox "hi"\r\nEnd Sub\r\n')
220
+ project.add_module("MyClass", "Option Explicit\r\n", kind=VBAModuleKind.other)
221
+ project.rename_module("OldName", "NewName")
222
+ project.delete_module("Obsolete")
223
+ wb.save("out.xlsm")
224
+ ```
225
+
226
+ ```python
227
+ from pyopenvba import AccessDatabase, VBAModuleKind
228
+
229
+ with AccessDatabase("database.accdb") as db:
230
+ project = db.vba_project()
231
+ project.add_module("Helpers", "Option Compare Database\r\n\r\nPublic Function Twice(n As Long) As Long\r\n Twice = n * 2\r\nEnd Function")
232
+ project.add_module("Widget", "Option Compare Database", kind=VBAModuleKind.other)
233
+ project.rename_module("Helpers", "Tools")
234
+ project.delete_module("Widget")
235
+ db.save()
236
+ ```
237
+
238
+ A class source is accepted in any form: a bare body (the header is
239
+ synthesized), a `.cls` file exported from the VBE (the `VERSION ... CLASS`
240
+ preamble is stripped and the `Attribute VB_Base` line restored), or a
241
+ full stream-form source. `db.references()`, `db.add_reference(...)` and
242
+ `db.drop_reference(...)` manage the libraries an Access project points
243
+ at.
244
+
245
+ ---
246
+
247
+ ## Edit your macros as files on disk
248
+
249
+ The easiest way to keep VBA in a git repo: export every module to a
250
+ folder, edit the files in any editor, push the changes back.
251
+
252
+ ```bash
253
+ python -m pyopenvba pull workbook.xlsm ./vba # every module to ./vba/*.bas and *.cls
254
+ python -m pyopenvba push ./vba workbook.xlsm # edits back into the workbook
255
+ python -m pyopenvba ls workbook.xlsm # list modules without extracting
256
+
257
+ python -m pyopenvba access-pull database.accdb ./vba
258
+ python -m pyopenvba access-push ./vba database.accdb
259
+ python -m pyopenvba access-ls database.accdb
260
+ ```
261
+
262
+ The same from Python, one pair per host:
263
+
264
+ ```python
265
+ from pyopenvba import pull, push, pull_word, push_word, pull_ppt, push_ppt, pull_access, push_access
266
+
267
+ pull("workbook.xlsm", "./vba")
268
+ push("./vba", "workbook.xlsm", out="edited.xlsm") # omit out= to save in place
269
+
270
+ pull_word("document.docm", "./vba")
271
+ push_word("./vba", "document.docm")
272
+
273
+ pull_ppt("presentation.pptm", "./vba")
274
+ push_ppt("./vba", "presentation.pptm")
275
+
276
+ pull_access("database.accdb", "./vba")
277
+ push_access("./vba", "database.accdb")
278
+ ```
279
+
280
+ Module files use the extensions VBA already uses: `.bas` for standard
281
+ modules, `.cls` for class modules and code-behind. `push` replaces the
282
+ source of every module that has a file of its name; a file that matches
283
+ no module is skipped, or refused with `strict=True`.
284
+
285
+ ---
286
+
287
+ ## Forms
288
+
289
+ A form's code is a module like any other. Its design, which controls
290
+ exist, how they nest and what their properties are, lives beside it and
291
+ is read and written with the same calls on every host.
292
+
293
+ ### UserForms in Excel, Word and PowerPoint
294
+
295
+ ```python
296
+ import pyopenvba
297
+
298
+ with pyopenvba.ExcelFile("book.xlsm") as wb:
299
+ for form in wb.forms():
300
+ print(form.name, len(form.walk()), "controls")
301
+ for control in form.walk():
302
+ print(f" {control.name:<16} {control.kind:<22} {control.properties()}")
303
+
304
+ form = wb.forms()[0]
305
+ form.control("OkButton").set_property("Caption", "Save")
306
+ form.control("NameBox").set_property("MaxLength", 40)
307
+ form.add_control("Label", "Hint", left=12, top=120, width=200)
308
+ form.remove_control("OldCheckbox")
309
+ wb.save()
310
+ ```
311
+
312
+ Containers work too. A `Frame` gets a storage of its own and removing it
313
+ takes its children; a `MultiPage` arrives with the two pages Excel gives
314
+ it, and pages are added and removed through it:
315
+
316
+ ```python
317
+ form.add_control("Frame", "Shipping", left=12, top=160, width=200, height=80)
318
+ form.add_control("OptionButton", "Ground", container="Shipping")
319
+ form.add_control("MultiPage", "Wizard", left=12, top=40, width=300, height=200)
320
+ form.add_page("Wizard", name="Review", caption="Review && confirm")
321
+ form.remove_page("Page2", multipage="Wizard")
322
+ ```
323
+
324
+ A form can be built from nothing. `add_form` creates the designer storage
325
+ and the code-behind module together:
326
+
327
+ ```python
328
+ with pyopenvba.ExcelFile("book.xlsm") as wb:
329
+ form = wb.add_form("Wizard", caption="Setup", width=300, height=200)
330
+ form.add_control("Label", "Prompt", left=12, top=12, width=200)
331
+ form.add_control("TextBox", "Answer", left=12, top=40, width=200)
332
+ form.add_control("CommandButton", "Ok", left=12, top=80)
333
+ wb.set_module("Wizard", "Private Sub Ok_Click()\r\n Me.Hide\r\nEnd Sub\r\n")
334
+ wb.save()
335
+ ```
336
+
337
+ Geometry is in points, the unit the designer shows. `set_property(name,
338
+ None)` clears a property, so the control goes back to its default.
339
+ MSForms stores a property only when it differs from the control's
340
+ default, so `properties()` returns what the developer set, which no live
341
+ host can tell you. Writing is lossless: an unedited form saves back byte
342
+ for byte.
343
+
344
+ The command line shows the tree:
345
+
346
+ ```bash
347
+ python -m pyopenvba forms book.xlsm
348
+ ```
349
+
350
+ ### Forms and reports in Access
351
+
352
+ Access forms and reports read and edit through the same surface. Sizes
353
+ are in twips, the unit Access keeps, and a report takes `kind="report"`:
354
+
355
+ ```python
356
+ from pyopenvba import AccessDatabase
357
+
358
+ with AccessDatabase("app.accdb") as db:
359
+ for form in db.forms():
360
+ print(form.name, [s.name for s in form.sections])
361
+ for control in form.walk():
362
+ print(" ", control.name, control.kind, control.properties().get("Caption"))
363
+
364
+ form = db.add_form("Summary", caption="Totals", width=8000, height=3000)
365
+ form.add_control("Label", "Title", left=240, top=240, width=2000, height=300, caption="Hello")
366
+ form.add_control("TextBox", "Total", top=700, caption="=1+1")
367
+ form.control("Title").set_property("FontSize", 14)
368
+ form.remove_control("Total")
369
+ form.set_code("Option Compare Database\r\n\r\nPrivate Sub Form_Load()\r\n Me.Caption = \"Loaded\"\r\nEnd Sub")
370
+
371
+ report = db.add_report("Monthly")
372
+ report.add_control("Label", "Banner", section="PageHeaderSection", caption="Header band")
373
+ db.delete_form("Old")
374
+ db.save()
375
+ ```
376
+
377
+ [examples/access_form_demo.py](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/examples/access_form_demo.py)
378
+ builds a working order calculator this way: a form whose buttons call a
379
+ standard module and keep their running total in a class module, laid out
380
+ and coloured through `set_property` (fonts, fills, borders, hover
381
+ colours, a currency format) and opened with the database through
382
+ `db.set_database_properties({"StartUpForm": "Calculator"})`, with the
383
+ database it produces beside it. Twenty-three control types can be
384
+ written, including a tab control and its pages
385
+ (`form.add_control("Page", "First", parent="Tabs")`); a navigation
386
+ control is read but not written. Each control gets only the
387
+ properties Access's own designs give its type, and `set_property` refuses
388
+ a name the type does not have. A live gate opens every written design in
389
+ Access's designer and reads back each control, measurement, caption and
390
+ tab index.
391
+
392
+ ---
393
+
394
+ ## Supported formats
395
+
396
+ ### Excel
397
+
398
+ | Extension | What it is | Read | Write | create_new |
399
+ |-----------|------------------------------|:----:|:-----:|:----------:|
400
+ | `.xlsm` | Macro-enabled workbook | yes | yes | yes |
401
+ | `.xlsb` | Binary workbook | yes | yes | yes |
402
+ | `.xlam` | Macro-enabled add-in | yes | yes | yes |
403
+ | `.xls` | Legacy (Excel 97-2003) | yes | yes | no |
404
+
405
+ ### Word
406
+
407
+ | Extension | What it is | Read | Write | create_new |
408
+ |-----------|------------------------------|:----:|:-----:|:----------:|
409
+ | `.docm` | Macro-enabled document | yes | yes | yes |
410
+ | `.dotm` | Macro-enabled template | yes | yes | no |
411
+ | `.doc` | Legacy (Word 97-2003) | yes | yes | no |
412
+
413
+ ### PowerPoint
414
+
415
+ | Extension | What it is | Read | Write | create_new |
416
+ |-----------|------------------------------|:----:|:-----:|:----------:|
417
+ | `.pptm` | Macro-enabled presentation | yes | yes | yes |
418
+ | `.potm` | Macro-enabled template | yes | yes | no |
419
+ | `.ppt` | Legacy (PowerPoint 97-2003) | yes | yes | no |
420
+
421
+ ### Access
422
+
423
+ | Extension | What it is | Read | Write | create_new |
424
+ |-----------|------------------------------|:----:|:-----:|:----------:|
425
+ | `.accdb` | Access database (ACE) | yes | yes | yes |
426
+ | `.mdb` | Access database (Jet 4) | yes | yes | no |
427
+
428
+ An Access file keeps its VBA project inside the database itself, in the
429
+ system tables Access uses for its own objects, so writing a module means
430
+ writing rows, long values and index entries the way the database engine
431
+ does. `AccessDatabase` does that with a pure-Python implementation of the
432
+ Jet 4 / ACE storage engine, documented rule by rule with how each was
433
+ measured in
434
+ [docs/access_engine.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/access_engine.md).
435
+ Two things follow. A written module has no compiled copy until Access
436
+ recompiles the project on its next open, which Access does on its own.
437
+ And `AccessReader`, the older read-only class, is still there for
438
+ inspecting a database: `vba_modules()`, `read_project_info()`,
439
+ `identifiers()`, `disassemble_module()` and the `MSysObjects` catalog.
440
+
441
+ Every save is verified to reopen in the host application without the
442
+ "we found a problem with some content" repair dialog.
443
+
444
+ ---
445
+
446
+ ## Safety guards
447
+
448
+ `save()` refuses to silently produce a broken file.
449
+
450
+ ### Password-protected projects
451
+
452
+ A mutation to a password-protected project raises `VBAProjectError`
453
+ unless you opt in:
454
+
455
+ ```python
456
+ wb.save(allow_protected=True)
457
+ ```
458
+
459
+ `AccessDatabase` does the same: `db.vba_is_protected()` says whether the
460
+ project carries a password, and `db.save()` refuses a VBA change to a
461
+ protected project without `allow_protected=True`. The library never
462
+ decrypts or changes the password; the protection bytes are preserved and
463
+ the file still asks for the original password in the VBE.
464
+
465
+ ### Digitally signed projects
466
+
467
+ Any change to the macros invalidates a digital signature. On mutation the
468
+ library drops the stale signature streams and emits a `UserWarning`:
469
+
470
+ ```python
471
+ import warnings
472
+ warnings.filterwarnings("error", category=UserWarning) # treat as fatal
473
+
474
+ wb.save(allow_invalidate_signature=True) # or accept it
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Out of scope
480
+
481
+ Preserved byte for byte but not interpreted:
482
+
483
+ - VBA project password decryption or re-encryption.
484
+ - Re-signing digitally signed projects.
485
+ - ActiveX license editing.
486
+
487
+ [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md)
488
+ has the feature matrix.
489
+
490
+ ---
491
+
492
+ ## Architecture
493
+
494
+ ```
495
+ src/pyopenvba/
496
+ __init__.py public API: ExcelFile, WordFile, PowerPointFile,
497
+ AccessDatabase, AccessReader, VBAForm, FormControl,
498
+ pull/push for each host, VBAModuleKind, exceptions
499
+ _host.py VBAHostFile: shared open/edit/pull/push/save pipeline
500
+ excel.py ExcelFile (VBAHostFile subclass, create_new template)
501
+ word.py WordFile
502
+ powerpoint.py PowerPointFile (.ppt overrides the container hooks)
503
+ access/ AccessDatabase: the VBA project, forms and reports,
504
+ and the Jet 4 / ACE storage engine they live in
505
+ access_read.py AccessReader: the older read-only inspector
506
+ vba.py VBA project parser and MS-OVBA codec
507
+ vba_pcode.py VBA7 p-code disassembler
508
+ cfb.py MS-CFB (Compound File Binary) parser/writer
509
+ forms.py UserForm designer streams: control tree, read and write
510
+ _oforms_records.py [MS-OFORMS] property table, one per control class
511
+ _oforms_pages.py a MultiPage's tabs and page bookkeeping
512
+ _ppt_container.py the VBA project a binary .ppt hides in its document stream
513
+ exceptions.py exception hierarchy
514
+ _templates/ empty .xlsm/.xlsb/.xlam/.docm/.pptm/.accdb bytes for create_new()
515
+ __main__.py python -m pyopenvba {pull,push,ls,forms,disasm,access-ls,access-pull,access-push,access-disasm}
516
+ ```
517
+
518
+ For more:
519
+
520
+ - [docs/architecture.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/architecture.md): internal layout and conventions.
521
+ - [docs/access_engine.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/access_engine.md): the Access file format as measured, and what the engine reproduces.
522
+ - [docs/ms-ovba-implementation-guide_v2.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/ms-ovba-implementation-guide_v2.md): a language-agnostic guide to re-implementing MS-OVBA.
523
+ - [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md): per-feature status.
524
+
525
+ ---
526
+
527
+ ## Contributing
528
+
529
+ Bug reports, files that break the library, and pull requests are welcome.
530
+ Please include the file, or a minimal redacted version, when filing a
531
+ parsing bug.
532
+
533
+ Run the same checks as CI:
534
+
535
+ ```bash
536
+ pip install -e ".[dev]"
537
+ pyright src tests
538
+ pytest -p no:randomly
539
+ ```
540
+
541
+ On Windows with desktop Office installed you can also run the live gates,
542
+ which are skipped by default and in CI. Each builds a file with pyOpenVBA
543
+ and has the real application open it, run its code, or perform the same
544
+ edit for a byte-for-byte comparison:
545
+
546
+ ```powershell
547
+ $env:RUN_LIVE_EXCEL = "1"; pytest tests/test_live_excel_gate.py
548
+ $env:RUN_LIVE_ACCESS = "1"; pytest tests/test_live_access_engine_gate.py
549
+ $env:RUN_LIVE_ACCESS_VBA = "1"; pytest tests/test_live_access_design_gate.py
550
+ ```
551
+
552
+ CI runs the test matrix on Python 3.10 through 3.14 on Linux, plus 3.12
553
+ on Windows and macOS, on every push and pull request. Releases go to
554
+ PyPI when a `v*.*.*` tag is pushed.
555
+
556
+ ---
557
+
558
+ ## License
559
+
560
+ [MIT](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md).
561
+
562
+ ---
563
+
564
+ ## Support open source
565
+
566
+ If pyOpenVBA saves you time or helps your team keep VBA maintainable,
567
+ support keeps the project moving.
568
+
569
+ - [GitHub Sponsors](https://github.com/sponsors/WilliamSmithEdward)
570
+ - [PayPal](https://www.paypal.com/donate/?business=ML855BRLNR838&no_recurring=0&item_name=VBA+has+always+treated+me+well.+It+was+how+I+first+grew+professional+as+a+programmer%2C+I%27m+happy+to+show+it+some+love+%E2%9D%A4%EF%B8%8F&currency_code=USD)
571
+ - [Cash App](https://cash.app/$williamesmithjcil)