pyOpenVBA 6.3.5__tar.gz → 6.5.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 (236) hide show
  1. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/PKG-INFO +68 -9
  2. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/README.md +67 -8
  3. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/architecture.md +7 -0
  4. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/pyproject.toml +1 -1
  5. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/__init__.py +10 -1
  6. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_host.py +51 -12
  7. pyopenvba-6.5.0/src/pyopenvba/_package_copy.py +172 -0
  8. pyopenvba-6.5.0/src/pyopenvba/integrity.py +506 -0
  9. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_opc.py +3 -1
  10. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/.gitignore +0 -0
  11. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/LICENSE.md +0 -0
  12. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/excel_checklist.csv +0 -0
  13. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/host_completeness.md +0 -0
  14. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  15. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/power_query.md +0 -0
  16. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/research/access_complex/README.md +0 -0
  17. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/research/access_designs/README.md +0 -0
  18. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/research/access_macros/README.md +0 -0
  19. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/research/access_write/README.md +0 -0
  20. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/research/pcode/README.md +0 -0
  21. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/roadmap.md +0 -0
  22. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/docs/vba_runtime.md +0 -0
  23. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/__main__.py +0 -0
  24. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_a1.py +0 -0
  25. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_deflate.py +0 -0
  26. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_new_project.py +0 -0
  27. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_oforms_pages.py +0 -0
  28. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_oforms_records.py +0 -0
  29. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_package_signature.py +0 -0
  30. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_ppt_container.py +0 -0
  31. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_references.py +0 -0
  32. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_relationships.py +0 -0
  33. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/__init__.py +0 -0
  34. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  35. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
  36. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  37. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  38. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  39. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  40. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  41. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
  42. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
  43. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
  44. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.blob +0 -0
  45. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
  46. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
  47. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
  48. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.blob +0 -0
  49. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
  50. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
  51. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
  52. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/_xml.py +0 -0
  53. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/__init__.py +0 -0
  54. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_alloc.py +0 -0
  55. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_btree.py +0 -0
  56. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_collation.py +0 -0
  57. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
  58. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_compact.py +0 -0
  59. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_complex.py +0 -0
  60. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_datapage.py +0 -0
  61. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_ddl.py +0 -0
  62. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_designs.py +0 -0
  63. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_facade.py +0 -0
  64. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_format.py +0 -0
  65. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_index.py +0 -0
  66. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_lval.py +0 -0
  67. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_macros.py +0 -0
  68. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_pages.py +0 -0
  69. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_props.py +0 -0
  70. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_queries.py +0 -0
  71. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_rows.py +0 -0
  72. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_schema.py +0 -0
  73. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_sql.py +0 -0
  74. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_storage.py +0 -0
  75. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_tdef.py +0 -0
  76. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_themes.py +0 -0
  77. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_validate.py +0 -0
  78. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/_vba.py +0 -0
  79. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access/database.py +0 -0
  80. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/access_read.py +0 -0
  81. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/__init__.py +0 -0
  82. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/__init__.py +0 -0
  83. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_arrays.py +0 -0
  84. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_autofill.py +0 -0
  85. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_autofilter.py +0 -0
  86. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_bridge.py +0 -0
  87. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_calc.py +0 -0
  88. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_cell_styles.py +0 -0
  89. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_clipboard.py +0 -0
  90. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_control_refs.py +0 -0
  91. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_controls.py +0 -0
  92. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_copy_names.py +0 -0
  93. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_dimensions.py +0 -0
  94. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_duplicates.py +0 -0
  95. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_dynamic_file.py +0 -0
  96. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_editing.py +0 -0
  97. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_engine_book.py +0 -0
  98. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_evaluate.py +0 -0
  99. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_events.py +0 -0
  100. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_find.py +0 -0
  101. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_font_rows.py +0 -0
  102. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_formats.py +0 -0
  103. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_formula_format.py +0 -0
  104. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_hyperlinks.py +0 -0
  105. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_hyperlinks_file.py +0 -0
  106. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_io.py +0 -0
  107. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_merges.py +0 -0
  108. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_model.py +0 -0
  109. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_named_api.py +0 -0
  110. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_names.py +0 -0
  111. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_notes.py +0 -0
  112. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_notes_file.py +0 -0
  113. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_number_format.py +0 -0
  114. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_protection.py +0 -0
  115. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_radios.py +0 -0
  116. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_refresh.py +0 -0
  117. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_region.py +0 -0
  118. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_row_formats.py +0 -0
  119. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_shape_api.py +0 -0
  120. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_shapes.py +0 -0
  121. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_shared.py +0 -0
  122. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_sheet_copy.py +0 -0
  123. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_sheet_move.py +0 -0
  124. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_sort.py +0 -0
  125. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_special_cells.py +0 -0
  126. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_spills.py +0 -0
  127. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_styles.py +0 -0
  128. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_table_edits.py +0 -0
  129. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_tables.py +0 -0
  130. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_text_columns.py +0 -0
  131. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_typing.py +0 -0
  132. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_validation.py +0 -0
  133. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_visible.py +0 -0
  134. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_windows.py +0 -0
  135. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/excel/_worksheet_functions.py +0 -0
  136. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/__init__.py +0 -0
  137. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/_bridge.py +0 -0
  138. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/_io.py +0 -0
  139. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/powerpoint/_model.py +0 -0
  140. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/__init__.py +0 -0
  141. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/_bridge.py +0 -0
  142. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/_io.py +0 -0
  143. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/apps/word/_model.py +0 -0
  144. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/cfb.py +0 -0
  145. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/excel.py +0 -0
  146. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/exceptions.py +0 -0
  147. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/forms.py +0 -0
  148. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/__init__.py +0 -0
  149. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_arity.py +0 -0
  150. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/__init__.py +0 -0
  151. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/catalog.py +0 -0
  152. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/cells.py +0 -0
  153. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/collate.py +0 -0
  154. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/dates.py +0 -0
  155. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/evaluator.py +0 -0
  156. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/__init__.py +0 -0
  157. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/aggregate.py +0 -0
  158. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/arithmetic.py +0 -0
  159. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/arrays.py +0 -0
  160. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/common.py +0 -0
  161. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/conditional.py +0 -0
  162. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/database.py +0 -0
  163. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/dates.py +0 -0
  164. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/distributions.py +0 -0
  165. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/engineering.py +0 -0
  166. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/financial.py +0 -0
  167. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/information.py +0 -0
  168. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/lambdas.py +0 -0
  169. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/logical.py +0 -0
  170. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/lookup.py +0 -0
  171. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/matrix.py +0 -0
  172. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/statistics.py +0 -0
  173. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/subtotal.py +0 -0
  174. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/functions/text.py +0 -0
  175. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/host.py +0 -0
  176. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/lexer.py +0 -0
  177. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/nodes.py +0 -0
  178. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/numbers.py +0 -0
  179. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/parser.py +0 -0
  180. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/precise.py +0 -0
  181. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/reference.py +0 -0
  182. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/registry.py +0 -0
  183. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/special.py +0 -0
  184. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_calc/values.py +0 -0
  185. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_deep.py +0 -0
  186. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_display.py +0 -0
  187. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_formula2.py +0 -0
  188. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_inventory.py +0 -0
  189. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_parse.py +0 -0
  190. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_prefixes.py +0 -0
  191. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_r1c1.py +0 -0
  192. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_spell.py +0 -0
  193. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_structured.py +0 -0
  194. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/formula/_values.py +0 -0
  195. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/__init__.py +0 -0
  196. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_ast.py +0 -0
  197. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_constants_data.py +0 -0
  198. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_intrinsics.py +0 -0
  199. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_inventory.py +0 -0
  200. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_inventory_data.py +0 -0
  201. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_lex.py +0 -0
  202. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_objects.py +0 -0
  203. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_parse.py +0 -0
  204. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_preprocess.py +0 -0
  205. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_runtime.py +0 -0
  206. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_scripting.py +0 -0
  207. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/interpreter/_values.py +0 -0
  208. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/mlang/__init__.py +0 -0
  209. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_eval.py +0 -0
  210. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_inventory.py +0 -0
  211. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_inventory_data.py +0 -0
  212. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_library.py +0 -0
  213. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_parse.py +0 -0
  214. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/mlang/_values.py +0 -0
  215. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerpoint.py +0 -0
  216. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/__init__.py +0 -0
  217. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_binary.py +0 -0
  218. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_files.py +0 -0
  219. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_mashup.py +0 -0
  220. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_metadata.py +0 -0
  221. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_package.py +0 -0
  222. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_refresh.py +0 -0
  223. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_section.py +0 -0
  224. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/_sheets.py +0 -0
  225. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/powerquery/workbook.py +0 -0
  226. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/shapes/__init__.py +0 -0
  227. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_docx.py +0 -0
  228. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_docx_text.py +0 -0
  229. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_drawingml.py +0 -0
  230. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_pptx.py +0 -0
  231. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_values.py +0 -0
  232. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/shapes/_xlsx.py +0 -0
  233. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/vba.py +0 -0
  234. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/vba_pcode.py +0 -0
  235. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/src/pyopenvba/word.py +0 -0
  236. {pyopenvba-6.3.5 → pyopenvba-6.5.0}/tests/fuzz_corpus/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyOpenVBA
3
- Version: 6.3.5
3
+ Version: 6.5.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,15 +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
47
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/15116/badge)](https://www.bestpractices.dev/projects/15116)
47
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
48
- [![Downloads](https://static.pepy.tech/badge/pyOpenVBA/month)](https://pepy.tech/project/pyOpenVBA)
48
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
49
49
 
50
50
  **Read and write the code inside Office files, in pure Python: VBA macros
51
51
  in four hosts, and Power Query in Excel.**
@@ -985,7 +985,66 @@ Every save is verified to reopen in the host application without the
985
985
 
986
986
  ## Safety guards
987
987
 
988
- `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
+ ### File integrity checks and conservative repair
997
+
998
+ Check a file explicitly before or after editing, independently of the fast
999
+ save path:
1000
+
1001
+ ```python
1002
+ from pyopenvba import check_file, repair_file, FileRepairError
1003
+
1004
+ report = check_file("book.xlsm")
1005
+ for issue in report.issues:
1006
+ print(issue.severity, issue.code, issue.location, issue.message)
1007
+
1008
+ if report.ok:
1009
+ print("All supported integrity checks passed")
1010
+ elif report.complete and report.errors and all(i.repairable for i in report.errors):
1011
+ try:
1012
+ result = repair_file("book.xlsm", output="book.repaired.xlsm")
1013
+ print(result.changes, result.after.ok)
1014
+ except FileRepairError as exc:
1015
+ print(exc.report.issues)
1016
+ ```
1017
+
1018
+ `check_file()` returns a `FileCheckReport` containing `errors`, `warnings`,
1019
+ `checked_parts`, and `complete`. `ok` requires no errors and complete coverage
1020
+ of the supported checks. ZIP parts are decompressed and CRC checked; local
1021
+ headers, data descriptors, XML, content types, and internal relationships are
1022
+ checked too. CFB directory pointers and stream sizes are checked, and supported
1023
+ VBA projects have their module sources and forms parsed. VBA is never executed.
1024
+ These checks do not prove that Office can open the document, compile its VBA,
1025
+ or verify its digital signatures.
1026
+
1027
+ Repair initially supports **stale local ZIP CRC/size fields only**, when the
1028
+ actual payload passes central-directory verification. It preserves every
1029
+ other byte and checks the candidate before writing a new file. It refuses
1030
+ missing parts, corrupt payloads, ambiguous entries, and incomplete checks;
1031
+ `FileRepairError.report` explains the refusal. An existing output is never
1032
+ overwritten, and a healthy input can be copied without changes.
1033
+
1034
+ The default limits are 256 MiB per uncompressed part and 1 GiB for both input
1035
+ size and the sum of declared uncompressed sizes. Raise `max_part_bytes` and
1036
+ `max_total_bytes` explicitly for trusted larger files. Invalid limits, oversized
1037
+ input, missing paths, and filesystem failures raise exceptions. A skipped part
1038
+ is reported as incomplete. ZIP64 payloads are verified, but local ZIP64 size
1039
+ metadata is currently reported as incomplete. Unknown VBA formats and Access
1040
+ coverage are also explicit warnings: Access checks its catalog and VBA, not
1041
+ every table row or allocation map. Access's existing `compact_and_repair()`
1042
+ returns a rebuilt database and remains a separate operation.
1043
+
1044
+ Encrypted ZIP parts and compression methods other than stored/deflate are
1045
+ reported as incomplete and cannot be repaired by this API.
1046
+
1047
+ `save()` enforces the following protection and signature gates.
989
1048
 
990
1049
  ### Password-protected projects
991
1050
 
@@ -1,14 +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
10
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/15116/badge)](https://www.bestpractices.dev/projects/15116)
10
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
11
- [![Downloads](https://static.pepy.tech/badge/pyOpenVBA/month)](https://pepy.tech/project/pyOpenVBA)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
12
12
 
13
13
  **Read and write the code inside Office files, in pure Python: VBA macros
14
14
  in four hosts, and Power Query in Excel.**
@@ -948,7 +948,66 @@ Every save is verified to reopen in the host application without the
948
948
 
949
949
  ## Safety guards
950
950
 
951
- `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
+ ### File integrity checks and conservative repair
960
+
961
+ Check a file explicitly before or after editing, independently of the fast
962
+ save path:
963
+
964
+ ```python
965
+ from pyopenvba import check_file, repair_file, FileRepairError
966
+
967
+ report = check_file("book.xlsm")
968
+ for issue in report.issues:
969
+ print(issue.severity, issue.code, issue.location, issue.message)
970
+
971
+ if report.ok:
972
+ print("All supported integrity checks passed")
973
+ elif report.complete and report.errors and all(i.repairable for i in report.errors):
974
+ try:
975
+ result = repair_file("book.xlsm", output="book.repaired.xlsm")
976
+ print(result.changes, result.after.ok)
977
+ except FileRepairError as exc:
978
+ print(exc.report.issues)
979
+ ```
980
+
981
+ `check_file()` returns a `FileCheckReport` containing `errors`, `warnings`,
982
+ `checked_parts`, and `complete`. `ok` requires no errors and complete coverage
983
+ of the supported checks. ZIP parts are decompressed and CRC checked; local
984
+ headers, data descriptors, XML, content types, and internal relationships are
985
+ checked too. CFB directory pointers and stream sizes are checked, and supported
986
+ VBA projects have their module sources and forms parsed. VBA is never executed.
987
+ These checks do not prove that Office can open the document, compile its VBA,
988
+ or verify its digital signatures.
989
+
990
+ Repair initially supports **stale local ZIP CRC/size fields only**, when the
991
+ actual payload passes central-directory verification. It preserves every
992
+ other byte and checks the candidate before writing a new file. It refuses
993
+ missing parts, corrupt payloads, ambiguous entries, and incomplete checks;
994
+ `FileRepairError.report` explains the refusal. An existing output is never
995
+ overwritten, and a healthy input can be copied without changes.
996
+
997
+ The default limits are 256 MiB per uncompressed part and 1 GiB for both input
998
+ size and the sum of declared uncompressed sizes. Raise `max_part_bytes` and
999
+ `max_total_bytes` explicitly for trusted larger files. Invalid limits, oversized
1000
+ input, missing paths, and filesystem failures raise exceptions. A skipped part
1001
+ is reported as incomplete. ZIP64 payloads are verified, but local ZIP64 size
1002
+ metadata is currently reported as incomplete. Unknown VBA formats and Access
1003
+ coverage are also explicit warnings: Access checks its catalog and VBA, not
1004
+ every table row or allocation map. Access's existing `compact_and_repair()`
1005
+ returns a rebuilt database and remains a separate operation.
1006
+
1007
+ Encrypted ZIP parts and compression methods other than stored/deflate are
1008
+ reported as incomplete and cannot be repaired by this API.
1009
+
1010
+ `save()` enforces the following protection and signature gates.
952
1011
 
953
1012
  ### Password-protected projects
954
1013
 
@@ -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.5"
7
+ version = "6.5.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" }
@@ -81,6 +81,9 @@ from pyopenvba.exceptions import (
81
81
  VBAProjectError,
82
82
  )
83
83
  from pyopenvba.forms import FormControl, Size, VBAForm
84
+ from pyopenvba.integrity import (
85
+ FileCheckReport, FileIssue, FileRepairError, FileRepairResult, check_file, repair_file,
86
+ )
84
87
  from pyopenvba.powerpoint import PowerPointFile
85
88
  from pyopenvba.powerquery import PowerQuery, PowerQueryWorkbook, QueryGroup, RefreshSettings
86
89
  from pyopenvba.powerquery import pull_queries as _pull_queries
@@ -228,6 +231,12 @@ def push_access(
228
231
 
229
232
 
230
233
  __all__ = [
234
+ "FileCheckReport",
235
+ "FileIssue",
236
+ "FileRepairError",
237
+ "FileRepairResult",
238
+ "check_file",
239
+ "repair_file",
231
240
  "AccessDatabase",
232
241
  "AccessReader",
233
242
  "CFBError",
@@ -293,4 +302,4 @@ def push_power_query(
293
302
  """
294
303
  return _push_queries(src_dir, workbook, out=out, encoding=encoding, remove_missing=remove_missing)
295
304
 
296
- __version__ = "6.3.5"
305
+ __version__ = "6.5.0"
@@ -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
@@ -108,6 +109,12 @@ class VBAHostFile(ReferenceManager):
108
109
  # Set by add_vba_project: the project is not in the package yet, and these are its document modules.
109
110
  self._project_added = False
110
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
111
118
  self._open()
112
119
 
113
120
  # ------------------------------------------------------------------
@@ -251,7 +258,7 @@ class VBAHostFile(ReferenceManager):
251
258
  if not self._has_project:
252
259
  return SignatureInfo()
253
260
  info = detect_signature(self._get_cfb())
254
- if self._zip is not None:
261
+ if self._zip is not None and not self._signature_edits:
255
262
  parts = signature_parts(self._zip.namelist(), self._zip.read, self._vba_entry)
256
263
  info.kinds += [kind for kind in dict.fromkeys(parts.values()) if kind not in info.kinds]
257
264
  info.parts = list(parts)
@@ -501,6 +508,7 @@ class VBAHostFile(ReferenceManager):
501
508
  *,
502
509
  allow_protected: bool = False,
503
510
  allow_invalidate_signature: bool = False,
511
+ full_rebuild: bool = False,
504
512
  ) -> None:
505
513
  """
506
514
  Save the file, applying any pending module edits.
@@ -510,7 +518,14 @@ class VBAHostFile(ReferenceManager):
510
518
  Only the ``vbaProject.bin`` entry is rewritten; every other ZIP
511
519
  entry is preserved byte-for-byte along with its compression
512
520
  method and metadata so the file's non-VBA structure remains
513
- 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``.
514
529
 
515
530
  Safety gates:
516
531
 
@@ -533,8 +548,8 @@ class VBAHostFile(ReferenceManager):
533
548
  to a UserForm's design counts: adding or removing a control
534
549
  changes the form class's members.
535
550
 
536
- A file with no VBA project can hold no edit, since every write
537
- 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.
538
553
 
539
554
  Templates and add-ins that share a supported layout (``.xltm``,
540
555
  ``.xlt``, ``.xla``, ``.dot``, ``.ppsm``, ``.ppam``) open read only:
@@ -545,7 +560,9 @@ class VBAHostFile(ReferenceManager):
545
560
  f"Saving a {self._suffix} file is not supported; it opens read only."
546
561
  )
547
562
  if not self._has_project:
548
- 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:
549
566
  Path(dest).write_bytes(self._container_raw)
550
567
  return
551
568
  cfb = self._get_cfb()
@@ -561,9 +578,11 @@ class VBAHostFile(ReferenceManager):
561
578
  for form in self._forms or ():
562
579
  # Not any(...): that short-circuits, and every form has to be
563
580
  # written, not just the first dirty one.
564
- forms_dirty |= form.write_back(cfb)
581
+ changed = form.write_back(cfb)
582
+ forms_dirty |= changed
583
+ self._pending_mutation |= changed
565
584
  # What the package's other parts need: None drops a part.
566
- package_edits: dict[str, bytes | None] = {}
585
+ package_edits = dict(self._signature_edits)
567
586
 
568
587
  if self._project is not None:
569
588
  project = self._project
@@ -575,7 +594,8 @@ class VBAHostFile(ReferenceManager):
575
594
  delete_names = set(project.pending_deletes)
576
595
  has_source_edits = any(m.dirty for m in project.modules)
577
596
  mutating = bool(
578
- rename_map
597
+ self._pending_mutation
598
+ or rename_map
579
599
  or add_names
580
600
  or delete_names
581
601
  or has_source_edits
@@ -598,6 +618,8 @@ class VBAHostFile(ReferenceManager):
598
618
  f"the {self._host_noun} inconsistent)."
599
619
  )
600
620
 
621
+ self._pending_mutation |= mutating
622
+
601
623
  # Safety gate 2: any change invalidates a present digital
602
624
  # signature. Drop it and warn.
603
625
  if mutating:
@@ -617,7 +639,8 @@ class VBAHostFile(ReferenceManager):
617
639
  except KeyError:
618
640
  pass
619
641
  new_cfb_bytes = cfb.to_bytes()
620
- 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
621
644
 
622
645
  def _apply_project(self, cfb: CFB, project: VBAProject, *, mutating: bool) -> None:
623
646
  """Write ``project``'s pending renames, adds, deletes and edits into ``cfb``."""
@@ -744,12 +767,13 @@ class VBAHostFile(ReferenceManager):
744
767
  if mutating:
745
768
  invalidate_vba_project_cache(cfb)
746
769
 
747
- def _write_container(self, dest: str | Path | None, new_cfb_bytes: bytes,
748
- 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:
749
772
  """Write the file with ``new_cfb_bytes`` as its project and ``package_edits`` applied."""
750
773
  out_path = Path(dest) if dest is not None else self._path
751
774
 
752
775
  if self._suffix in self._cfb_formats:
776
+ assert new_cfb_bytes is not None
753
777
  out_path.write_bytes(self._container_bytes(new_cfb_bytes))
754
778
  return
755
779
 
@@ -765,12 +789,24 @@ class VBAHostFile(ReferenceManager):
765
789
  package_edits.update(self._project_edits())
766
790
  if self._writes_project(self._project):
767
791
  package_edits.update(with_project(self._zip.namelist(), self._zip.read, self._main_part))
792
+ assert new_cfb_bytes is not None
768
793
  added[self._vba_entry] = new_cfb_bytes
769
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
+
770
806
  buf = io.BytesIO()
771
807
  with zipfile.ZipFile(buf, "w") as out_zip:
772
808
  for info in self._zip.infolist():
773
- if info.filename == self._vba_entry:
809
+ if info.filename == self._vba_entry and new_cfb_bytes is not None:
774
810
  out_info = zipfile.ZipInfo(
775
811
  filename=info.filename,
776
812
  date_time=info.date_time,
@@ -812,6 +848,8 @@ class VBAHostFile(ReferenceManager):
812
848
  zip-based file keeps beside it come back as the package's edits,
813
849
  None for each part that goes (see :mod:`pyopenvba._package_signature`).
814
850
  """
851
+ if self._signature_edits:
852
+ return dict(self._signature_edits)
815
853
  found = detect_signature(cfb)
816
854
  if found.present:
817
855
  for sig_stream in (
@@ -834,6 +872,7 @@ class VBAHostFile(ReferenceManager):
834
872
  kinds += [kind for kind in signature_parts(names, self._zip.read, self._vba_entry).values()
835
873
  if kind not in kinds]
836
874
  edits = without_signature(names, self._zip.read, self._vba_entry)
875
+ self._signature_edits.update(edits)
837
876
  if kinds and not allow_invalidate_signature:
838
877
  warnings.warn(
839
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