pyOpenVBA 6.3.4__tar.gz → 6.4.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-6.3.4 → pyopenvba-6.4.0}/PKG-INFO +27 -12
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/README.md +26 -11
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/architecture.md +7 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/pyproject.toml +1 -1
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/__init__.py +1 -1
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/__main__.py +6 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_host.py +61 -12
- pyopenvba-6.4.0/src/pyopenvba/_package_copy.py +172 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_pages.py +2 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_rows.py +19 -3
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_tdef.py +8 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/database.py +17 -1
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/cfb.py +21 -1
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/excel.py +5 -2
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_parse.py +8 -2
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerpoint.py +3 -1
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_metadata.py +1 -1
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_opc.py +3 -1
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_package.py +13 -2
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/vba.py +21 -1
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/word.py +3 -1
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/tests/fuzz_corpus/README.md +18 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/.gitignore +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/LICENSE.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/excel_checklist.csv +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/host_completeness.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/power_query.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/access_complex/README.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/access_designs/README.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/access_macros/README.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/access_write/README.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/pcode/README.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/roadmap.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/vba_runtime.md +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_a1.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_deflate.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_new_project.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_oforms_pages.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_oforms_records.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_package_signature.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_ppt_container.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_references.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_relationships.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.blob +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.blob +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_xml.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_alloc.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_btree.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_collation.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_compact.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_complex.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_datapage.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_ddl.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_designs.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_facade.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_format.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_index.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_lval.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_macros.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_props.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_queries.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_schema.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_sql.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_storage.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_themes.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_validate.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_vba.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access_read.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_arrays.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_autofill.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_autofilter.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_bridge.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_calc.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_cell_styles.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_clipboard.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_control_refs.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_controls.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_copy_names.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_dimensions.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_duplicates.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_dynamic_file.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_editing.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_engine_book.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_evaluate.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_events.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_find.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_font_rows.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_formats.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_formula_format.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_hyperlinks.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_hyperlinks_file.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_io.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_merges.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_model.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_named_api.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_names.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_notes.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_notes_file.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_number_format.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_protection.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_radios.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_refresh.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_region.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_row_formats.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_shape_api.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_shapes.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_shared.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_sheet_copy.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_sheet_move.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_sort.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_special_cells.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_spills.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_styles.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_table_edits.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_tables.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_text_columns.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_typing.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_validation.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_visible.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_windows.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_worksheet_functions.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/_bridge.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/_io.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/_model.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/_bridge.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/_io.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/_model.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/forms.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_arity.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/catalog.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/cells.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/collate.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/dates.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/evaluator.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/aggregate.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/arithmetic.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/arrays.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/common.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/conditional.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/database.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/dates.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/distributions.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/engineering.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/financial.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/information.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/lambdas.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/logical.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/lookup.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/matrix.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/statistics.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/subtotal.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/text.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/host.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/lexer.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/nodes.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/numbers.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/parser.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/precise.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/reference.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/registry.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/special.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/values.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_deep.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_display.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_formula2.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_inventory.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_parse.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_prefixes.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_r1c1.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_spell.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_structured.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_values.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_ast.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_constants_data.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_intrinsics.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_inventory.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_inventory_data.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_lex.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_objects.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_parse.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_preprocess.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_runtime.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_scripting.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_values.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_eval.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_inventory.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_inventory_data.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_library.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_values.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_binary.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_files.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_mashup.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_refresh.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_section.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_sheets.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/workbook.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/__init__.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_docx.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_docx_text.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_drawingml.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_pptx.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_values.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_xlsx.py +0 -0
- {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/vba_pcode.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyOpenVBA
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.4.0
|
|
4
4
|
Summary: Read and write VBA macros and Excel Power Query 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
|
|
@@ -37,14 +37,15 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
# pyOpenVBA
|
|
39
39
|
|
|
40
|
-
[](https://pypi.org/project/pyOpenVBA/)
|
|
41
|
+
[](https://pypi.org/project/pyOpenVBA/)
|
|
42
|
+
[](https://pepy.tech/projects/pyOpenVBA)
|
|
43
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
|
|
44
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml)
|
|
45
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml)
|
|
45
46
|
[](https://scorecard.dev/viewer/?uri=github.com/WilliamSmithEdward/pyOpenVBA)
|
|
46
|
-
[](https://www.bestpractices.dev/projects/15116)
|
|
48
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
|
|
48
49
|
|
|
49
50
|
**Read and write the code inside Office files, in pure Python: VBA macros
|
|
50
51
|
in four hosts, and Power Query in Excel.**
|
|
@@ -54,9 +55,9 @@ Works on Windows, macOS and Linux. Python 3.10 or newer.
|
|
|
54
55
|
|
|
55
56
|
VBA, four hosts and one API:
|
|
56
57
|
|
|
57
|
-
* Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`)
|
|
58
|
-
* Word (`.docm`, `.dotm`, `.doc`)
|
|
59
|
-
* PowerPoint (`.pptm`, `.potm`, `.ppt`)
|
|
58
|
+
* Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`; `.xltm`, `.xlt`, `.xla` read only)
|
|
59
|
+
* Word (`.docm`, `.dotm`, `.doc`; `.dot` read only)
|
|
60
|
+
* PowerPoint (`.pptm`, `.potm`, `.ppt`; `.ppsm`, `.ppam` read only)
|
|
60
61
|
* Access (`.accdb`, `.mdb`)
|
|
61
62
|
|
|
62
63
|
Power Query, in any Excel package:
|
|
@@ -886,6 +887,9 @@ implemented, what is not, and how it was measured is in
|
|
|
886
887
|
| `.xlsb` | Binary workbook | yes | yes | yes |
|
|
887
888
|
| `.xlam` | Macro-enabled add-in | yes | yes | yes |
|
|
888
889
|
| `.xls` | Legacy (Excel 97-2003) | yes | - | no |
|
|
890
|
+
| `.xltm` | Macro-enabled template | read | yes | no |
|
|
891
|
+
| `.xlt` | Legacy template | read | - | no |
|
|
892
|
+
| `.xla` | Legacy add-in | read | - | no |
|
|
889
893
|
|
|
890
894
|
A `.xlsx` file has no VBA project by design, and Power Query lives
|
|
891
895
|
outside the project, so it is read and written in every package above.
|
|
@@ -897,6 +901,7 @@ outside the project, so it is read and written in every package above.
|
|
|
897
901
|
| `.docm` | Macro-enabled document | yes | yes | yes |
|
|
898
902
|
| `.dotm` | Macro-enabled template | yes | yes | no |
|
|
899
903
|
| `.doc` | Legacy (Word 97-2003) | yes | yes | no |
|
|
904
|
+
| `.dot` | Legacy template | yes | no | no |
|
|
900
905
|
|
|
901
906
|
### PowerPoint
|
|
902
907
|
|
|
@@ -904,6 +909,8 @@ outside the project, so it is read and written in every package above.
|
|
|
904
909
|
|-----------|------------------------------|:----:|:-----:|:----------:|
|
|
905
910
|
| `.pptm` | Macro-enabled presentation | yes | yes | yes |
|
|
906
911
|
| `.potm` | Macro-enabled template | yes | yes | no |
|
|
912
|
+
| `.ppsm` | Macro-enabled show | yes | no | no |
|
|
913
|
+
| `.ppam` | Macro-enabled add-in | yes | no | no |
|
|
907
914
|
| `.ppt` | Legacy (PowerPoint 97-2003) | yes | yes | no |
|
|
908
915
|
|
|
909
916
|
### Files with no VBA project
|
|
@@ -978,7 +985,15 @@ Every save is verified to reopen in the host application without the
|
|
|
978
985
|
|
|
979
986
|
## Safety guards
|
|
980
987
|
|
|
981
|
-
|
|
988
|
+
ZIP-based VBA saves copy untouched compressed parts by default, avoiding
|
|
989
|
+
recompression of the rest of the document. Existing corruption in those parts
|
|
990
|
+
is preserved without checking their compressed data or CRC. Use
|
|
991
|
+
`wb.save(full_rebuild=True)` (also on `WordFile` and `PowerPointFile`) to
|
|
992
|
+
decompress and rewrite every retained part and detect such damage. This does
|
|
993
|
+
not repair damaged parts. Unsupported packages fall back to a full rebuild;
|
|
994
|
+
legacy binary formats ignore this flag.
|
|
995
|
+
|
|
996
|
+
`save()` enforces the following protection and signature gates.
|
|
982
997
|
|
|
983
998
|
### Password-protected projects
|
|
984
999
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# pyOpenVBA
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/pyOpenVBA/)
|
|
4
|
+
[](https://pypi.org/project/pyOpenVBA/)
|
|
5
|
+
[](https://pepy.tech/projects/pyOpenVBA)
|
|
6
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml)
|
|
8
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml)
|
|
8
9
|
[](https://scorecard.dev/viewer/?uri=github.com/WilliamSmithEdward/pyOpenVBA)
|
|
9
|
-
[](https://www.bestpractices.dev/projects/15116)
|
|
11
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
|
|
11
12
|
|
|
12
13
|
**Read and write the code inside Office files, in pure Python: VBA macros
|
|
13
14
|
in four hosts, and Power Query in Excel.**
|
|
@@ -17,9 +18,9 @@ Works on Windows, macOS and Linux. Python 3.10 or newer.
|
|
|
17
18
|
|
|
18
19
|
VBA, four hosts and one API:
|
|
19
20
|
|
|
20
|
-
* Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`)
|
|
21
|
-
* Word (`.docm`, `.dotm`, `.doc`)
|
|
22
|
-
* PowerPoint (`.pptm`, `.potm`, `.ppt`)
|
|
21
|
+
* Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`; `.xltm`, `.xlt`, `.xla` read only)
|
|
22
|
+
* Word (`.docm`, `.dotm`, `.doc`; `.dot` read only)
|
|
23
|
+
* PowerPoint (`.pptm`, `.potm`, `.ppt`; `.ppsm`, `.ppam` read only)
|
|
23
24
|
* Access (`.accdb`, `.mdb`)
|
|
24
25
|
|
|
25
26
|
Power Query, in any Excel package:
|
|
@@ -849,6 +850,9 @@ implemented, what is not, and how it was measured is in
|
|
|
849
850
|
| `.xlsb` | Binary workbook | yes | yes | yes |
|
|
850
851
|
| `.xlam` | Macro-enabled add-in | yes | yes | yes |
|
|
851
852
|
| `.xls` | Legacy (Excel 97-2003) | yes | - | no |
|
|
853
|
+
| `.xltm` | Macro-enabled template | read | yes | no |
|
|
854
|
+
| `.xlt` | Legacy template | read | - | no |
|
|
855
|
+
| `.xla` | Legacy add-in | read | - | no |
|
|
852
856
|
|
|
853
857
|
A `.xlsx` file has no VBA project by design, and Power Query lives
|
|
854
858
|
outside the project, so it is read and written in every package above.
|
|
@@ -860,6 +864,7 @@ outside the project, so it is read and written in every package above.
|
|
|
860
864
|
| `.docm` | Macro-enabled document | yes | yes | yes |
|
|
861
865
|
| `.dotm` | Macro-enabled template | yes | yes | no |
|
|
862
866
|
| `.doc` | Legacy (Word 97-2003) | yes | yes | no |
|
|
867
|
+
| `.dot` | Legacy template | yes | no | no |
|
|
863
868
|
|
|
864
869
|
### PowerPoint
|
|
865
870
|
|
|
@@ -867,6 +872,8 @@ outside the project, so it is read and written in every package above.
|
|
|
867
872
|
|-----------|------------------------------|:----:|:-----:|:----------:|
|
|
868
873
|
| `.pptm` | Macro-enabled presentation | yes | yes | yes |
|
|
869
874
|
| `.potm` | Macro-enabled template | yes | yes | no |
|
|
875
|
+
| `.ppsm` | Macro-enabled show | yes | no | no |
|
|
876
|
+
| `.ppam` | Macro-enabled add-in | yes | no | no |
|
|
870
877
|
| `.ppt` | Legacy (PowerPoint 97-2003) | yes | yes | no |
|
|
871
878
|
|
|
872
879
|
### Files with no VBA project
|
|
@@ -941,7 +948,15 @@ Every save is verified to reopen in the host application without the
|
|
|
941
948
|
|
|
942
949
|
## Safety guards
|
|
943
950
|
|
|
944
|
-
|
|
951
|
+
ZIP-based VBA saves copy untouched compressed parts by default, avoiding
|
|
952
|
+
recompression of the rest of the document. Existing corruption in those parts
|
|
953
|
+
is preserved without checking their compressed data or CRC. Use
|
|
954
|
+
`wb.save(full_rebuild=True)` (also on `WordFile` and `PowerPointFile`) to
|
|
955
|
+
decompress and rewrite every retained part and detect such damage. This does
|
|
956
|
+
not repair damaged parts. Unsupported packages fall back to a full rebuild;
|
|
957
|
+
legacy binary formats ignore this flag.
|
|
958
|
+
|
|
959
|
+
`save()` enforces the following protection and signature gates.
|
|
945
960
|
|
|
946
961
|
### Password-protected projects
|
|
947
962
|
|
|
@@ -313,6 +313,13 @@ For the ZIP case, the VBA project is at the fixed path
|
|
|
313
313
|
`xl/vbaProject.bin`. On save, exactly that entry is replaced while
|
|
314
314
|
every other ZIP entry is preserved byte-for-byte including its
|
|
315
315
|
compression method, external attributes, create system, and timestamp.
|
|
316
|
+
By default, unchanged parts keep their compressed bytes (`_package_copy.py`),
|
|
317
|
+
avoiding decompression and recompression. They are not checked for damaged
|
|
318
|
+
compressed data or CRC mismatches. `save(full_rebuild=True)` uses the previous
|
|
319
|
+
`zipfile` writer, which decompresses and rewrites every retained part and
|
|
320
|
+
refuses damaged ones. ZIP64 and other unsupported packages fall back to that
|
|
321
|
+
writer automatically. Copying still takes time proportional to compressed
|
|
322
|
+
package size; it avoids work proportional to the expanded content.
|
|
316
323
|
|
|
317
324
|
For `.xls` and `.doc`, the entire file *is* the CFB; `cfb.to_bytes()`
|
|
318
325
|
is written straight to the output path.
|
|
@@ -179,12 +179,18 @@ _HOST_BY_SUFFIX: dict[str, type] = {
|
|
|
179
179
|
".xlsm": ExcelFile,
|
|
180
180
|
".xlsb": ExcelFile,
|
|
181
181
|
".xlam": ExcelFile,
|
|
182
|
+
".xltm": ExcelFile,
|
|
182
183
|
".xls": ExcelFile,
|
|
184
|
+
".xlt": ExcelFile,
|
|
185
|
+
".xla": ExcelFile,
|
|
183
186
|
".docm": WordFile,
|
|
184
187
|
".dotm": WordFile,
|
|
185
188
|
".doc": WordFile,
|
|
189
|
+
".dot": WordFile,
|
|
186
190
|
".pptm": PowerPointFile,
|
|
187
191
|
".potm": PowerPointFile,
|
|
192
|
+
".ppsm": PowerPointFile,
|
|
193
|
+
".ppam": PowerPointFile,
|
|
188
194
|
".ppt": PowerPointFile,
|
|
189
195
|
}
|
|
190
196
|
|
|
@@ -20,6 +20,7 @@ from pathlib import Path
|
|
|
20
20
|
from typing import ClassVar, TypeVar
|
|
21
21
|
|
|
22
22
|
from pyopenvba._new_project import with_project
|
|
23
|
+
from pyopenvba._package_copy import copy_package
|
|
23
24
|
from pyopenvba._package_signature import signature_parts, without_signature
|
|
24
25
|
from pyopenvba._references import ReferenceManager, module_offset, reference_spans
|
|
25
26
|
from pyopenvba.cfb import CFB
|
|
@@ -91,6 +92,8 @@ class VBAHostFile(ReferenceManager):
|
|
|
91
92
|
_project_storage: ClassVar[str | None]
|
|
92
93
|
#: The formats add_vba_project gives a project to, each measured against its application.
|
|
93
94
|
_project_formats: ClassVar[frozenset[str]] = frozenset()
|
|
95
|
+
#: Formats that open and read but do not save: a writer for them has not been measured.
|
|
96
|
+
_read_only_formats: ClassVar[frozenset[str]] = frozenset()
|
|
94
97
|
#: The package part a zip-based file's project is related from.
|
|
95
98
|
_main_part: ClassVar[str] = ""
|
|
96
99
|
|
|
@@ -106,6 +109,12 @@ class VBAHostFile(ReferenceManager):
|
|
|
106
109
|
# Set by add_vba_project: the project is not in the package yet, and these are its document modules.
|
|
107
110
|
self._project_added = False
|
|
108
111
|
self._document_names: set[str] = set()
|
|
112
|
+
# Package signature edits survive repeated saves and failed writes;
|
|
113
|
+
# the original ZIP remains the source of unchanged parts.
|
|
114
|
+
self._signature_edits: dict[str, bytes | None] = {}
|
|
115
|
+
# Edits already applied to the in-memory CFB are still mutations
|
|
116
|
+
# until a write succeeds, even if their dirty flags were consumed.
|
|
117
|
+
self._pending_mutation = False
|
|
109
118
|
self._open()
|
|
110
119
|
|
|
111
120
|
# ------------------------------------------------------------------
|
|
@@ -249,7 +258,7 @@ class VBAHostFile(ReferenceManager):
|
|
|
249
258
|
if not self._has_project:
|
|
250
259
|
return SignatureInfo()
|
|
251
260
|
info = detect_signature(self._get_cfb())
|
|
252
|
-
if self._zip is not None:
|
|
261
|
+
if self._zip is not None and not self._signature_edits:
|
|
253
262
|
parts = signature_parts(self._zip.namelist(), self._zip.read, self._vba_entry)
|
|
254
263
|
info.kinds += [kind for kind in dict.fromkeys(parts.values()) if kind not in info.kinds]
|
|
255
264
|
info.parts = list(parts)
|
|
@@ -499,6 +508,7 @@ class VBAHostFile(ReferenceManager):
|
|
|
499
508
|
*,
|
|
500
509
|
allow_protected: bool = False,
|
|
501
510
|
allow_invalidate_signature: bool = False,
|
|
511
|
+
full_rebuild: bool = False,
|
|
502
512
|
) -> None:
|
|
503
513
|
"""
|
|
504
514
|
Save the file, applying any pending module edits.
|
|
@@ -508,7 +518,14 @@ class VBAHostFile(ReferenceManager):
|
|
|
508
518
|
Only the ``vbaProject.bin`` entry is rewritten; every other ZIP
|
|
509
519
|
entry is preserved byte-for-byte along with its compression
|
|
510
520
|
method and metadata so the file's non-VBA structure remains
|
|
511
|
-
intact.
|
|
521
|
+
intact. By default, unchanged parts keep their compressed bytes;
|
|
522
|
+
they are not decompressed or checked for corruption. Set
|
|
523
|
+
``full_rebuild=True`` to decompress and rewrite every retained
|
|
524
|
+
part, checking its compressed data and CRC in the process. This
|
|
525
|
+
detects damage; it does not repair it. Packages the copying writer
|
|
526
|
+
cannot handle safely fall back to a full rebuild automatically.
|
|
527
|
+
Legacy raw-CFB formats write the CFB bytes directly and ignore
|
|
528
|
+
``full_rebuild``.
|
|
512
529
|
|
|
513
530
|
Safety gates:
|
|
514
531
|
|
|
@@ -531,11 +548,21 @@ class VBAHostFile(ReferenceManager):
|
|
|
531
548
|
to a UserForm's design counts: adding or removing a control
|
|
532
549
|
changes the form class's members.
|
|
533
550
|
|
|
534
|
-
A file with no VBA project
|
|
535
|
-
|
|
551
|
+
A file with no VBA project is copied as read unless
|
|
552
|
+
``full_rebuild=True`` requests rebuilding its ZIP container.
|
|
553
|
+
|
|
554
|
+
Templates and add-ins that share a supported layout (``.xltm``,
|
|
555
|
+
``.xlt``, ``.xla``, ``.dot``, ``.ppsm``, ``.ppam``) open read only:
|
|
556
|
+
saving one raises :class:`~pyopenvba.exceptions.UnsupportedFormatError`.
|
|
536
557
|
"""
|
|
558
|
+
if self._suffix in self._read_only_formats:
|
|
559
|
+
raise UnsupportedFormatError(
|
|
560
|
+
f"Saving a {self._suffix} file is not supported; it opens read only."
|
|
561
|
+
)
|
|
537
562
|
if not self._has_project:
|
|
538
|
-
if
|
|
563
|
+
if full_rebuild and self._suffix in self._zip_formats:
|
|
564
|
+
self._write_container(dest, None, {}, full_rebuild=True)
|
|
565
|
+
elif dest is not None:
|
|
539
566
|
Path(dest).write_bytes(self._container_raw)
|
|
540
567
|
return
|
|
541
568
|
cfb = self._get_cfb()
|
|
@@ -551,9 +578,11 @@ class VBAHostFile(ReferenceManager):
|
|
|
551
578
|
for form in self._forms or ():
|
|
552
579
|
# Not any(...): that short-circuits, and every form has to be
|
|
553
580
|
# written, not just the first dirty one.
|
|
554
|
-
|
|
581
|
+
changed = form.write_back(cfb)
|
|
582
|
+
forms_dirty |= changed
|
|
583
|
+
self._pending_mutation |= changed
|
|
555
584
|
# What the package's other parts need: None drops a part.
|
|
556
|
-
package_edits
|
|
585
|
+
package_edits = dict(self._signature_edits)
|
|
557
586
|
|
|
558
587
|
if self._project is not None:
|
|
559
588
|
project = self._project
|
|
@@ -565,7 +594,8 @@ class VBAHostFile(ReferenceManager):
|
|
|
565
594
|
delete_names = set(project.pending_deletes)
|
|
566
595
|
has_source_edits = any(m.dirty for m in project.modules)
|
|
567
596
|
mutating = bool(
|
|
568
|
-
|
|
597
|
+
self._pending_mutation
|
|
598
|
+
or rename_map
|
|
569
599
|
or add_names
|
|
570
600
|
or delete_names
|
|
571
601
|
or has_source_edits
|
|
@@ -588,6 +618,8 @@ class VBAHostFile(ReferenceManager):
|
|
|
588
618
|
f"the {self._host_noun} inconsistent)."
|
|
589
619
|
)
|
|
590
620
|
|
|
621
|
+
self._pending_mutation |= mutating
|
|
622
|
+
|
|
591
623
|
# Safety gate 2: any change invalidates a present digital
|
|
592
624
|
# signature. Drop it and warn.
|
|
593
625
|
if mutating:
|
|
@@ -607,7 +639,8 @@ class VBAHostFile(ReferenceManager):
|
|
|
607
639
|
except KeyError:
|
|
608
640
|
pass
|
|
609
641
|
new_cfb_bytes = cfb.to_bytes()
|
|
610
|
-
self._write_container(dest, new_cfb_bytes, package_edits)
|
|
642
|
+
self._write_container(dest, new_cfb_bytes, package_edits, full_rebuild=full_rebuild)
|
|
643
|
+
self._pending_mutation = False
|
|
611
644
|
|
|
612
645
|
def _apply_project(self, cfb: CFB, project: VBAProject, *, mutating: bool) -> None:
|
|
613
646
|
"""Write ``project``'s pending renames, adds, deletes and edits into ``cfb``."""
|
|
@@ -734,12 +767,13 @@ class VBAHostFile(ReferenceManager):
|
|
|
734
767
|
if mutating:
|
|
735
768
|
invalidate_vba_project_cache(cfb)
|
|
736
769
|
|
|
737
|
-
def _write_container(self, dest: str | Path | None, new_cfb_bytes: bytes,
|
|
738
|
-
package_edits: dict[str, bytes | None]) -> None:
|
|
770
|
+
def _write_container(self, dest: str | Path | None, new_cfb_bytes: bytes | None,
|
|
771
|
+
package_edits: dict[str, bytes | None], *, full_rebuild: bool = False) -> None:
|
|
739
772
|
"""Write the file with ``new_cfb_bytes`` as its project and ``package_edits`` applied."""
|
|
740
773
|
out_path = Path(dest) if dest is not None else self._path
|
|
741
774
|
|
|
742
775
|
if self._suffix in self._cfb_formats:
|
|
776
|
+
assert new_cfb_bytes is not None
|
|
743
777
|
out_path.write_bytes(self._container_bytes(new_cfb_bytes))
|
|
744
778
|
return
|
|
745
779
|
|
|
@@ -755,12 +789,24 @@ class VBAHostFile(ReferenceManager):
|
|
|
755
789
|
package_edits.update(self._project_edits())
|
|
756
790
|
if self._writes_project(self._project):
|
|
757
791
|
package_edits.update(with_project(self._zip.namelist(), self._zip.read, self._main_part))
|
|
792
|
+
assert new_cfb_bytes is not None
|
|
758
793
|
added[self._vba_entry] = new_cfb_bytes
|
|
759
794
|
|
|
795
|
+
# Avoid inflating and recompressing untouched parts. Unsupported
|
|
796
|
+
# packages and an explicit full rebuild retain the zipfile path.
|
|
797
|
+
if not full_rebuild:
|
|
798
|
+
edits = dict(package_edits)
|
|
799
|
+
if new_cfb_bytes is not None:
|
|
800
|
+
edits[self._vba_entry] = new_cfb_bytes
|
|
801
|
+
packed = copy_package(self._container_raw, self._zip.infolist(), edits, added)
|
|
802
|
+
if packed is not None:
|
|
803
|
+
out_path.write_bytes(packed)
|
|
804
|
+
return
|
|
805
|
+
|
|
760
806
|
buf = io.BytesIO()
|
|
761
807
|
with zipfile.ZipFile(buf, "w") as out_zip:
|
|
762
808
|
for info in self._zip.infolist():
|
|
763
|
-
if info.filename == self._vba_entry:
|
|
809
|
+
if info.filename == self._vba_entry and new_cfb_bytes is not None:
|
|
764
810
|
out_info = zipfile.ZipInfo(
|
|
765
811
|
filename=info.filename,
|
|
766
812
|
date_time=info.date_time,
|
|
@@ -802,6 +848,8 @@ class VBAHostFile(ReferenceManager):
|
|
|
802
848
|
zip-based file keeps beside it come back as the package's edits,
|
|
803
849
|
None for each part that goes (see :mod:`pyopenvba._package_signature`).
|
|
804
850
|
"""
|
|
851
|
+
if self._signature_edits:
|
|
852
|
+
return dict(self._signature_edits)
|
|
805
853
|
found = detect_signature(cfb)
|
|
806
854
|
if found.present:
|
|
807
855
|
for sig_stream in (
|
|
@@ -824,6 +872,7 @@ class VBAHostFile(ReferenceManager):
|
|
|
824
872
|
kinds += [kind for kind in signature_parts(names, self._zip.read, self._vba_entry).values()
|
|
825
873
|
if kind not in kinds]
|
|
826
874
|
edits = without_signature(names, self._zip.read, self._vba_entry)
|
|
875
|
+
self._signature_edits.update(edits)
|
|
827
876
|
if kinds and not allow_invalidate_signature:
|
|
828
877
|
warnings.warn(
|
|
829
878
|
f"Dropped the stale VBA digital signature ({', '.join(kinds)}) "
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Write a zip-based Office file with a few parts changed and the rest as they were stored.
|
|
2
|
+
|
|
3
|
+
Saving a VBA project changes one part, ``vbaProject.bin``, and at most a
|
|
4
|
+
handful of small parts beside it. Reading and recompressing everything
|
|
5
|
+
else costs time proportional to the expanded content. Copying unchanged
|
|
6
|
+
compressed bytes avoids that work, using the same container writer as
|
|
7
|
+
Power Query and the document surfaces. It still costs time and memory
|
|
8
|
+
proportional to the compressed package size.
|
|
9
|
+
A changed part is deflated by zlib at its default level, as ``zipfile``
|
|
10
|
+
deflates it, so it is stored as the same bytes as before this module
|
|
11
|
+
existed.
|
|
12
|
+
|
|
13
|
+
:func:`copy_package` answers None for a package it would not carry over
|
|
14
|
+
faithfully, and the caller then writes it with ``zipfile`` as it always
|
|
15
|
+
has. That covers ZIP64, a part compressed some other way than deflate
|
|
16
|
+
that has to be written anew, an encrypted part, a part name that is not
|
|
17
|
+
UTF-8, an unknown compression method, unsupported patched-data or
|
|
18
|
+
strong-encryption flags, and a package naming one part twice. It also
|
|
19
|
+
covers a package whose two directories disagree -- a part's own header naming it
|
|
20
|
+
differently, giving it other sizes in a ZIP64 record, or placing it over
|
|
21
|
+
another part or the central directory -- which ``zipfile`` refuses,
|
|
22
|
+
and so still does.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import struct
|
|
28
|
+
import zipfile
|
|
29
|
+
import zlib
|
|
30
|
+
from collections.abc import Mapping, Sequence
|
|
31
|
+
from dataclasses import replace
|
|
32
|
+
|
|
33
|
+
from pyopenvba.exceptions import PowerQueryError
|
|
34
|
+
from pyopenvba.powerquery._opc import Entry, OpcFile
|
|
35
|
+
|
|
36
|
+
_STORED = 0
|
|
37
|
+
_DEFLATED = 8
|
|
38
|
+
#: General purpose bit 0: the part is encrypted.
|
|
39
|
+
_ENCRYPTED = 0x0001
|
|
40
|
+
#: Patched data and strong encryption are refused by zipfile when reading.
|
|
41
|
+
_UNSUPPORTED_FLAGS = 0x0060
|
|
42
|
+
_UTF8 = 0x0800
|
|
43
|
+
#: General purpose bit 3: the sizes follow the part's bytes instead of standing in its header.
|
|
44
|
+
_DESCRIPTOR = 0x0008
|
|
45
|
+
#: The date ``zipfile`` gives a part with none, 1980-01-01, as [APPNOTE] 4.4.6 packs one.
|
|
46
|
+
_NO_DATE = 0x0021
|
|
47
|
+
#: A size or offset of this much is held in a ZIP64 record instead.
|
|
48
|
+
_ZIP64 = 0xFFFFFFFF
|
|
49
|
+
#: The extra field record that holds one.
|
|
50
|
+
_ZIP64_RECORD = 0x0001
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _has_zip64(extra: bytes) -> bool:
|
|
54
|
+
"""Whether an extra field holds a ZIP64 record, or cannot be read to say."""
|
|
55
|
+
at = 0
|
|
56
|
+
while at < len(extra):
|
|
57
|
+
if at + 4 > len(extra):
|
|
58
|
+
return True
|
|
59
|
+
kind, size = struct.unpack_from("<HH", extra, at)
|
|
60
|
+
if kind == _ZIP64_RECORD:
|
|
61
|
+
return True
|
|
62
|
+
at += 4 + size
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _pack(data: bytes, method: int) -> bytes | None:
|
|
67
|
+
"""``data`` as a part stored by ``method`` holds it; None for a method not written here."""
|
|
68
|
+
if method == _STORED:
|
|
69
|
+
return data
|
|
70
|
+
if method == _DEFLATED:
|
|
71
|
+
deflater = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15)
|
|
72
|
+
return deflater.compress(data) + deflater.flush()
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def copy_package(
|
|
77
|
+
raw: bytes,
|
|
78
|
+
infos: Sequence[zipfile.ZipInfo],
|
|
79
|
+
edits: Mapping[str, bytes | None],
|
|
80
|
+
added: Mapping[str, bytes],
|
|
81
|
+
) -> bytes | None:
|
|
82
|
+
"""The package ``raw`` with ``edits`` applied and ``added`` appended, every other part as it was stored.
|
|
83
|
+
|
|
84
|
+
``infos`` are the package's entries as ``zipfile`` read them, which
|
|
85
|
+
is what the edits are named by. ``edits`` gives a part its new
|
|
86
|
+
bytes, or None to leave it out; a part is kept in its place, under
|
|
87
|
+
its own compression method, dates and attributes. ``added`` are
|
|
88
|
+
parts the package does not have yet, deflated and put last.
|
|
89
|
+
|
|
90
|
+
None means the package is one this does not write; see the module's
|
|
91
|
+
docstring.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
package = OpcFile.parse(raw)
|
|
95
|
+
central_start = struct.unpack_from("<I", raw, raw.rfind(b"PK\x05\x06") + 16)[0]
|
|
96
|
+
except (PowerQueryError, UnicodeDecodeError, struct.error):
|
|
97
|
+
return None
|
|
98
|
+
if len(package.entries) != len(infos) or len({info.filename for info in infos}) != len(infos):
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
entries: list[Entry] = []
|
|
102
|
+
# Where the last part's bytes ended: each part has to start past the one before it.
|
|
103
|
+
end = 0
|
|
104
|
+
for entry, info in zip(package.entries, infos, strict=True):
|
|
105
|
+
# The two readers have to agree on what the part is, or an edit could land on the wrong one.
|
|
106
|
+
if (
|
|
107
|
+
entry.name != info.filename
|
|
108
|
+
or len(entry.body) != info.compress_size
|
|
109
|
+
or entry.uncompressed_size != info.file_size
|
|
110
|
+
or entry.crc != info.CRC
|
|
111
|
+
or entry.method not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED, zipfile.ZIP_BZIP2, zipfile.ZIP_LZMA)
|
|
112
|
+
or entry.flags & (_ENCRYPTED | _UNSUPPORTED_FLAGS)
|
|
113
|
+
or info.compress_size >= _ZIP64
|
|
114
|
+
or info.file_size >= _ZIP64
|
|
115
|
+
or _has_zip64(entry.local_extra)
|
|
116
|
+
or _has_zip64(entry.central_extra)
|
|
117
|
+
):
|
|
118
|
+
return None
|
|
119
|
+
# So do the part's own header and the directory: on its name, and on parts not lying over each other.
|
|
120
|
+
name = entry.name.encode("utf-8")
|
|
121
|
+
named = info.header_offset + 30
|
|
122
|
+
local_name_length, local_extra_length = struct.unpack_from("<HH", raw, info.header_offset + 26)
|
|
123
|
+
if (
|
|
124
|
+
local_name_length != len(name)
|
|
125
|
+
or local_extra_length != len(entry.local_extra)
|
|
126
|
+
or info.header_offset < end
|
|
127
|
+
or raw[named : named + local_name_length + local_extra_length] != name + entry.local_extra
|
|
128
|
+
):
|
|
129
|
+
return None
|
|
130
|
+
local_flags = struct.unpack_from("<H", raw, info.header_offset + 6)[0]
|
|
131
|
+
if (local_flags ^ entry.flags) & _UTF8:
|
|
132
|
+
return None
|
|
133
|
+
end = named + local_name_length + local_extra_length + len(entry.body)
|
|
134
|
+
if end > central_start:
|
|
135
|
+
return None
|
|
136
|
+
# The sizes are written in the header, so nothing follows the part's bytes to say them again.
|
|
137
|
+
flags = entry.flags & ~_DESCRIPTOR
|
|
138
|
+
if entry.name in edits:
|
|
139
|
+
data = edits[entry.name]
|
|
140
|
+
if data is None:
|
|
141
|
+
continue
|
|
142
|
+
body = _pack(data, entry.method)
|
|
143
|
+
if body is None:
|
|
144
|
+
return None
|
|
145
|
+
entry = replace(
|
|
146
|
+
entry, body=body, flags=flags, crc=zlib.crc32(data) & 0xFFFFFFFF, uncompressed_size=len(data)
|
|
147
|
+
)
|
|
148
|
+
elif flags != entry.flags:
|
|
149
|
+
entry = replace(entry, flags=flags)
|
|
150
|
+
entries.append(entry)
|
|
151
|
+
|
|
152
|
+
for name, data in added.items():
|
|
153
|
+
body = _pack(data, _DEFLATED)
|
|
154
|
+
assert body is not None
|
|
155
|
+
entries.append(
|
|
156
|
+
Entry(
|
|
157
|
+
name=name,
|
|
158
|
+
body=body,
|
|
159
|
+
method=_DEFLATED,
|
|
160
|
+
flags=0,
|
|
161
|
+
dos_time=0,
|
|
162
|
+
dos_date=_NO_DATE,
|
|
163
|
+
crc=zlib.crc32(data) & 0xFFFFFFFF,
|
|
164
|
+
uncompressed_size=len(data),
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
return OpcFile(entries=entries).serialize()
|
|
170
|
+
except struct.error:
|
|
171
|
+
# Past what a ZIP holds without ZIP64: more than 65535 parts, or 4 GB.
|
|
172
|
+
return None
|
|
@@ -409,6 +409,8 @@ def row_bytes(
|
|
|
409
409
|
target with ``overflow_target=True``, where the 0x8000 bit marks the
|
|
410
410
|
moved row rather than a deletion."""
|
|
411
411
|
slots = row_slots(page)
|
|
412
|
+
if not 0 <= slot < len(slots):
|
|
413
|
+
raise AccessError(f"slot {slot} out of range (0..{len(slots) - 1})")
|
|
412
414
|
entry = slots[slot]
|
|
413
415
|
if entry & ROW_DELETED and not overflow_target:
|
|
414
416
|
return None
|
|
@@ -184,9 +184,13 @@ def encode_text(text: str) -> bytes:
|
|
|
184
184
|
|
|
185
185
|
def decode_datetime(raw: bytes) -> _dt.datetime:
|
|
186
186
|
value = struct.unpack("<d", raw)[0]
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
try:
|
|
188
|
+
days = int(value)
|
|
189
|
+
fraction = abs(value - days)
|
|
190
|
+
return EPOCH + _dt.timedelta(days=days) + _dt.timedelta(days=fraction)
|
|
191
|
+
except (OverflowError, ValueError):
|
|
192
|
+
# Out of the year 1 to 9999, or not a number: a damaged value.
|
|
193
|
+
raise AccessError(f"date value {value!r} is outside the range a date can hold") from None
|
|
190
194
|
|
|
191
195
|
|
|
192
196
|
def encode_datetime(value: _dt.datetime) -> bytes:
|
|
@@ -455,11 +459,23 @@ def encode_row(
|
|
|
455
459
|
return bytes(row)
|
|
456
460
|
|
|
457
461
|
|
|
462
|
+
#: The bytes a fixed-size type's value takes; any other length is a damaged row.
|
|
463
|
+
_SCALAR_SIZES = {
|
|
464
|
+
TYPE_BYTE: 1, TYPE_INT: 2, TYPE_LONG: 4, TYPE_MONEY: 8, TYPE_FLOAT: 4, TYPE_DOUBLE: 8,
|
|
465
|
+
TYPE_DATETIME: 8, TYPE_GUID: 16, TYPE_COMPLEX: 4, TYPE_BIGINT: 8,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
458
469
|
def decode_scalar(column: ColumnDef, raw: bytes) -> object:
|
|
459
470
|
"""Decode a non-long-value column. ``raw`` is the present value."""
|
|
460
471
|
code = column.type_code
|
|
461
472
|
if code == TYPE_BOOLEAN:
|
|
462
473
|
return True
|
|
474
|
+
size = _SCALAR_SIZES.get(code, 0)
|
|
475
|
+
if size and len(raw) != size:
|
|
476
|
+
raise AccessError(
|
|
477
|
+
f"column {column.name!r} holds {len(raw)} bytes where its type takes {size}; the row is damaged"
|
|
478
|
+
)
|
|
463
479
|
if code == TYPE_BYTE:
|
|
464
480
|
return raw[0]
|
|
465
481
|
if code == TYPE_INT:
|
|
@@ -333,10 +333,14 @@ def read_definition_bytes(store: PageStore, page: int) -> tuple[bytes, list[int]
|
|
|
333
333
|
|
|
334
334
|
def _read_name(buf: bytes, pos: int) -> tuple[str, int]:
|
|
335
335
|
"""A name is prefixed by its length as a word, and stored as UTF-16."""
|
|
336
|
+
if pos + 2 > len(buf):
|
|
337
|
+
raise AccessError("name runs past the end of the definition")
|
|
336
338
|
length = struct.unpack_from("<H", buf, pos)[0]
|
|
337
339
|
raw = buf[pos + 2 : pos + 2 + length]
|
|
338
340
|
if len(raw) != length:
|
|
339
341
|
raise AccessError("name runs past the end of the definition")
|
|
342
|
+
if length % 2:
|
|
343
|
+
raise AccessError(f"name is {length} bytes, an odd length for UTF-16")
|
|
340
344
|
return raw.decode("utf-16-le"), pos + 2 + length
|
|
341
345
|
|
|
342
346
|
|
|
@@ -463,6 +467,10 @@ def parse_table_definition(store: PageStore, page: int) -> TableDefinition:
|
|
|
463
467
|
pos += 2
|
|
464
468
|
if number == INDEX_COLUMN_UNUSED:
|
|
465
469
|
break
|
|
470
|
+
if pos + 8 > len(buf):
|
|
471
|
+
raise AccessError(
|
|
472
|
+
f"table definition at page {page} ends inside its usage-map block"
|
|
473
|
+
)
|
|
466
474
|
column_usage_maps[number] = (u32(pos), u32(pos + 4))
|
|
467
475
|
pos += 8
|
|
468
476
|
|