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.
Files changed (235) hide show
  1. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/PKG-INFO +27 -12
  2. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/README.md +26 -11
  3. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/architecture.md +7 -0
  4. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/pyproject.toml +1 -1
  5. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/__init__.py +1 -1
  6. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/__main__.py +6 -0
  7. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_host.py +61 -12
  8. pyopenvba-6.4.0/src/pyopenvba/_package_copy.py +172 -0
  9. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_pages.py +2 -0
  10. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_rows.py +19 -3
  11. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_tdef.py +8 -0
  12. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/database.py +17 -1
  13. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/cfb.py +21 -1
  14. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/excel.py +5 -2
  15. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_parse.py +8 -2
  16. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerpoint.py +3 -1
  17. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_metadata.py +1 -1
  18. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_opc.py +3 -1
  19. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_package.py +13 -2
  20. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/vba.py +21 -1
  21. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/word.py +3 -1
  22. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/tests/fuzz_corpus/README.md +18 -0
  23. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/.gitignore +0 -0
  24. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/LICENSE.md +0 -0
  25. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/excel_checklist.csv +0 -0
  26. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/host_completeness.md +0 -0
  27. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  28. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/power_query.md +0 -0
  29. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/access_complex/README.md +0 -0
  30. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/access_designs/README.md +0 -0
  31. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/access_macros/README.md +0 -0
  32. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/access_write/README.md +0 -0
  33. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/research/pcode/README.md +0 -0
  34. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/roadmap.md +0 -0
  35. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/docs/vba_runtime.md +0 -0
  36. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_a1.py +0 -0
  37. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_deflate.py +0 -0
  38. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_new_project.py +0 -0
  39. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_oforms_pages.py +0 -0
  40. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_oforms_records.py +0 -0
  41. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_package_signature.py +0 -0
  42. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_ppt_container.py +0 -0
  43. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_references.py +0 -0
  44. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_relationships.py +0 -0
  45. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/__init__.py +0 -0
  46. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  47. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
  48. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  49. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  50. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  51. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  52. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  53. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
  54. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
  55. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
  56. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.blob +0 -0
  57. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
  58. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
  59. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
  60. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.blob +0 -0
  61. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
  62. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
  63. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
  64. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/_xml.py +0 -0
  65. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/__init__.py +0 -0
  66. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_alloc.py +0 -0
  67. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_btree.py +0 -0
  68. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_collation.py +0 -0
  69. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
  70. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_compact.py +0 -0
  71. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_complex.py +0 -0
  72. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_datapage.py +0 -0
  73. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_ddl.py +0 -0
  74. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_designs.py +0 -0
  75. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_facade.py +0 -0
  76. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_format.py +0 -0
  77. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_index.py +0 -0
  78. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_lval.py +0 -0
  79. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_macros.py +0 -0
  80. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_props.py +0 -0
  81. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_queries.py +0 -0
  82. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_schema.py +0 -0
  83. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_sql.py +0 -0
  84. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_storage.py +0 -0
  85. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_themes.py +0 -0
  86. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_validate.py +0 -0
  87. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access/_vba.py +0 -0
  88. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/access_read.py +0 -0
  89. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/__init__.py +0 -0
  90. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/__init__.py +0 -0
  91. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_arrays.py +0 -0
  92. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_autofill.py +0 -0
  93. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_autofilter.py +0 -0
  94. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_bridge.py +0 -0
  95. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_calc.py +0 -0
  96. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_cell_styles.py +0 -0
  97. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_clipboard.py +0 -0
  98. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_control_refs.py +0 -0
  99. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_controls.py +0 -0
  100. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_copy_names.py +0 -0
  101. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_dimensions.py +0 -0
  102. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_duplicates.py +0 -0
  103. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_dynamic_file.py +0 -0
  104. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_editing.py +0 -0
  105. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_engine_book.py +0 -0
  106. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_evaluate.py +0 -0
  107. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_events.py +0 -0
  108. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_find.py +0 -0
  109. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_font_rows.py +0 -0
  110. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_formats.py +0 -0
  111. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_formula_format.py +0 -0
  112. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_hyperlinks.py +0 -0
  113. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_hyperlinks_file.py +0 -0
  114. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_io.py +0 -0
  115. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_merges.py +0 -0
  116. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_model.py +0 -0
  117. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_named_api.py +0 -0
  118. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_names.py +0 -0
  119. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_notes.py +0 -0
  120. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_notes_file.py +0 -0
  121. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_number_format.py +0 -0
  122. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_protection.py +0 -0
  123. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_radios.py +0 -0
  124. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_refresh.py +0 -0
  125. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_region.py +0 -0
  126. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_row_formats.py +0 -0
  127. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_shape_api.py +0 -0
  128. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_shapes.py +0 -0
  129. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_shared.py +0 -0
  130. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_sheet_copy.py +0 -0
  131. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_sheet_move.py +0 -0
  132. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_sort.py +0 -0
  133. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_special_cells.py +0 -0
  134. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_spills.py +0 -0
  135. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_styles.py +0 -0
  136. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_table_edits.py +0 -0
  137. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_tables.py +0 -0
  138. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_text_columns.py +0 -0
  139. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_typing.py +0 -0
  140. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_validation.py +0 -0
  141. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_visible.py +0 -0
  142. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_windows.py +0 -0
  143. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_worksheet_functions.py +0 -0
  144. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/__init__.py +0 -0
  145. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/_bridge.py +0 -0
  146. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/_io.py +0 -0
  147. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/_model.py +0 -0
  148. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/__init__.py +0 -0
  149. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/_bridge.py +0 -0
  150. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/_io.py +0 -0
  151. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/_model.py +0 -0
  152. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/exceptions.py +0 -0
  153. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/forms.py +0 -0
  154. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/__init__.py +0 -0
  155. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_arity.py +0 -0
  156. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/__init__.py +0 -0
  157. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/catalog.py +0 -0
  158. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/cells.py +0 -0
  159. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/collate.py +0 -0
  160. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/dates.py +0 -0
  161. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/evaluator.py +0 -0
  162. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/__init__.py +0 -0
  163. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/aggregate.py +0 -0
  164. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/arithmetic.py +0 -0
  165. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/arrays.py +0 -0
  166. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/common.py +0 -0
  167. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/conditional.py +0 -0
  168. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/database.py +0 -0
  169. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/dates.py +0 -0
  170. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/distributions.py +0 -0
  171. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/engineering.py +0 -0
  172. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/financial.py +0 -0
  173. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/information.py +0 -0
  174. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/lambdas.py +0 -0
  175. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/logical.py +0 -0
  176. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/lookup.py +0 -0
  177. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/matrix.py +0 -0
  178. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/statistics.py +0 -0
  179. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/subtotal.py +0 -0
  180. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/text.py +0 -0
  181. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/host.py +0 -0
  182. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/lexer.py +0 -0
  183. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/nodes.py +0 -0
  184. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/numbers.py +0 -0
  185. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/parser.py +0 -0
  186. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/precise.py +0 -0
  187. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/reference.py +0 -0
  188. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/registry.py +0 -0
  189. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/special.py +0 -0
  190. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/values.py +0 -0
  191. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_deep.py +0 -0
  192. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_display.py +0 -0
  193. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_formula2.py +0 -0
  194. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_inventory.py +0 -0
  195. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_parse.py +0 -0
  196. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_prefixes.py +0 -0
  197. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_r1c1.py +0 -0
  198. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_spell.py +0 -0
  199. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_structured.py +0 -0
  200. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/formula/_values.py +0 -0
  201. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/__init__.py +0 -0
  202. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_ast.py +0 -0
  203. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_constants_data.py +0 -0
  204. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_intrinsics.py +0 -0
  205. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_inventory.py +0 -0
  206. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_inventory_data.py +0 -0
  207. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_lex.py +0 -0
  208. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_objects.py +0 -0
  209. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_parse.py +0 -0
  210. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_preprocess.py +0 -0
  211. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_runtime.py +0 -0
  212. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_scripting.py +0 -0
  213. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_values.py +0 -0
  214. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/__init__.py +0 -0
  215. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_eval.py +0 -0
  216. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_inventory.py +0 -0
  217. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_inventory_data.py +0 -0
  218. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_library.py +0 -0
  219. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_values.py +0 -0
  220. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/__init__.py +0 -0
  221. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_binary.py +0 -0
  222. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_files.py +0 -0
  223. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_mashup.py +0 -0
  224. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_refresh.py +0 -0
  225. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_section.py +0 -0
  226. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_sheets.py +0 -0
  227. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/workbook.py +0 -0
  228. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/__init__.py +0 -0
  229. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_docx.py +0 -0
  230. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_docx_text.py +0 -0
  231. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_drawingml.py +0 -0
  232. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_pptx.py +0 -0
  233. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_values.py +0 -0
  234. {pyopenvba-6.3.4 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_xlsx.py +0 -0
  235. {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.4
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
- [![PyPI version](https://img.shields.io/pypi/v/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
41
- [![Python versions](https://img.shields.io/pypi/pyversions/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
42
- [![CI](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml/badge.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
43
- [![Security](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml/badge.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml)
44
- [![Malware scan](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml/badge.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml)
40
+ [![PyPI version](https://img.shields.io/pypi/v/pyOpenVBA)](https://pypi.org/project/pyOpenVBA/)
41
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyOpenVBA)](https://pypi.org/project/pyOpenVBA/)
42
+ [![Downloads](https://static.pepy.tech/badge/pyOpenVBA/month)](https://pepy.tech/projects/pyOpenVBA)
43
+ [![CI](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
44
+ [![Security](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml)
45
+ [![Malware scan](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml/badge.svg?branch=main)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml)
45
46
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/WilliamSmithEdward/pyOpenVBA/badge)](https://scorecard.dev/viewer/?uri=github.com/WilliamSmithEdward/pyOpenVBA)
46
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
47
- [![Downloads](https://static.pepy.tech/badge/pyOpenVBA/month)](https://pepy.tech/project/pyOpenVBA)
47
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/15116/badge)](https://www.bestpractices.dev/projects/15116)
48
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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
- `save()` refuses to silently produce a broken file.
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
- [![PyPI version](https://img.shields.io/pypi/v/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
4
- [![Python versions](https://img.shields.io/pypi/pyversions/pyOpenVBA.svg)](https://pypi.org/project/pyOpenVBA/)
5
- [![CI](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml/badge.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
6
- [![Security](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml/badge.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml)
7
- [![Malware scan](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml/badge.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml)
3
+ [![PyPI version](https://img.shields.io/pypi/v/pyOpenVBA)](https://pypi.org/project/pyOpenVBA/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyOpenVBA)](https://pypi.org/project/pyOpenVBA/)
5
+ [![Downloads](https://static.pepy.tech/badge/pyOpenVBA/month)](https://pepy.tech/projects/pyOpenVBA)
6
+ [![CI](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
7
+ [![Security](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/security.yml)
8
+ [![Malware scan](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml/badge.svg?branch=main)](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/malware-scan.yml)
8
9
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/WilliamSmithEdward/pyOpenVBA/badge)](https://scorecard.dev/viewer/?uri=github.com/WilliamSmithEdward/pyOpenVBA)
9
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
10
- [![Downloads](https://static.pepy.tech/badge/pyOpenVBA/month)](https://pepy.tech/project/pyOpenVBA)
10
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/15116/badge)](https://www.bestpractices.dev/projects/15116)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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
- `save()` refuses to silently produce a broken file.
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.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyOpenVBA"
7
- version = "6.3.4"
7
+ version = "6.4.0"
8
8
  description = "Read and write VBA macros and Excel Power Query in pure Python, no dependencies."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -293,4 +293,4 @@ def push_power_query(
293
293
  """
294
294
  return _push_queries(src_dir, workbook, out=out, encoding=encoding, remove_missing=remove_missing)
295
295
 
296
- __version__ = "6.3.4"
296
+ __version__ = "6.4.0"
@@ -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. Legacy raw-CFB formats write the CFB bytes directly.
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 can hold no edit, since every write
535
- refuses, so it is written out as it was read.
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 dest is not None:
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
- forms_dirty |= form.write_back(cfb)
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: dict[str, bytes | None] = {}
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
- rename_map
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
- days = int(value)
188
- fraction = abs(value - days)
189
- return EPOCH + _dt.timedelta(days=days) + _dt.timedelta(days=fraction)
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