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.
- pyopenvba-4.0.0/PKG-INFO +571 -0
- pyopenvba-4.0.0/README.md +535 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/architecture.md +79 -106
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/ms-ovba-implementation-guide_v2.md +4 -4
- pyopenvba-4.0.0/docs/research/access_complex/README.md +78 -0
- pyopenvba-4.0.0/docs/research/access_designs/README.md +109 -0
- pyopenvba-4.0.0/docs/research/access_macros/README.md +135 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/research/access_write/README.md +455 -55
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/roadmap.md +19 -11
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/pyproject.toml +5 -2
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/__init__.py +35 -5
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/__main__.py +22 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.blob +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.lvprop +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.propdata +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.prototypes +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.blob +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.lvprop +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.propdata +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.prototypes +0 -0
- pyopenvba-4.0.0/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
- pyopenvba-4.0.0/src/pyopenvba/access/__init__.py +22 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_alloc.py +325 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_btree.py +429 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_collation.py +234 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_collation_general_legacy.py +5698 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_compact.py +567 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_complex.py +230 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_datapage.py +204 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_ddl.py +380 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_deflate.py +667 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_designs.py +2905 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_facade.py +362 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_format.py +282 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_index.py +514 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_lval.py +242 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_macros.py +161 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_pages.py +548 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_props.py +278 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_queries.py +660 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_rows.py +466 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_schema.py +670 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_sql.py +3283 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_storage.py +119 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_tdef.py +497 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_validate.py +171 -0
- pyopenvba-4.0.0/src/pyopenvba/access/_vba.py +573 -0
- pyopenvba-4.0.0/src/pyopenvba/access/database.py +4331 -0
- pyopenvba-3.5.0/PKG-INFO +0 -633
- pyopenvba-3.5.0/README.md +0 -597
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/.gitignore +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/LICENSE.md +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/docs/research/pcode/README.md +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_host.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_oforms_pages.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_oforms_records.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_ppt_container.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/__init__.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/access_read.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/cfb.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/excel.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/forms.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/powerpoint.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/vba.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/vba_pcode.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/src/pyopenvba/word.py +0 -0
- {pyopenvba-3.5.0 → pyopenvba-4.0.0}/tests/fuzz_corpus/README.md +0 -0
pyopenvba-4.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/pyOpenVBA/)
|
|
40
|
+
[](https://pypi.org/project/pyOpenVBA/)
|
|
41
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
|
|
42
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
|
|
43
|
+
[](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¤cy_code=USD)
|
|
571
|
+
- [Cash App](https://cash.app/$williamesmithjcil)
|