pyOpenVBA 6.3.5__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.5 → pyopenvba-6.4.0}/PKG-INFO +17 -9
  2. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/README.md +16 -8
  3. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/architecture.md +7 -0
  4. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/pyproject.toml +1 -1
  5. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/__init__.py +1 -1
  6. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_host.py +51 -12
  7. pyopenvba-6.4.0/src/pyopenvba/_package_copy.py +172 -0
  8. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_opc.py +3 -1
  9. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/.gitignore +0 -0
  10. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/LICENSE.md +0 -0
  11. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/excel_checklist.csv +0 -0
  12. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/host_completeness.md +0 -0
  13. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  14. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/power_query.md +0 -0
  15. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/research/access_complex/README.md +0 -0
  16. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/research/access_designs/README.md +0 -0
  17. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/research/access_macros/README.md +0 -0
  18. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/research/access_write/README.md +0 -0
  19. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/research/pcode/README.md +0 -0
  20. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/roadmap.md +0 -0
  21. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/docs/vba_runtime.md +0 -0
  22. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/__main__.py +0 -0
  23. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_a1.py +0 -0
  24. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_deflate.py +0 -0
  25. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_new_project.py +0 -0
  26. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_oforms_pages.py +0 -0
  27. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_oforms_records.py +0 -0
  28. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_package_signature.py +0 -0
  29. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_ppt_container.py +0 -0
  30. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_references.py +0 -0
  31. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_relationships.py +0 -0
  32. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/__init__.py +0 -0
  33. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  34. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
  35. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  36. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  37. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  38. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  39. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  40. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
  41. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
  42. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
  43. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.blob +0 -0
  44. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
  45. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
  46. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
  47. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.blob +0 -0
  48. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
  49. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
  50. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
  51. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/_xml.py +0 -0
  52. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/__init__.py +0 -0
  53. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_alloc.py +0 -0
  54. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_btree.py +0 -0
  55. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_collation.py +0 -0
  56. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
  57. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_compact.py +0 -0
  58. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_complex.py +0 -0
  59. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_datapage.py +0 -0
  60. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_ddl.py +0 -0
  61. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_designs.py +0 -0
  62. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_facade.py +0 -0
  63. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_format.py +0 -0
  64. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_index.py +0 -0
  65. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_lval.py +0 -0
  66. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_macros.py +0 -0
  67. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_pages.py +0 -0
  68. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_props.py +0 -0
  69. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_queries.py +0 -0
  70. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_rows.py +0 -0
  71. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_schema.py +0 -0
  72. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_sql.py +0 -0
  73. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_storage.py +0 -0
  74. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_tdef.py +0 -0
  75. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_themes.py +0 -0
  76. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_validate.py +0 -0
  77. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/_vba.py +0 -0
  78. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access/database.py +0 -0
  79. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/access_read.py +0 -0
  80. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/__init__.py +0 -0
  81. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/__init__.py +0 -0
  82. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_arrays.py +0 -0
  83. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_autofill.py +0 -0
  84. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_autofilter.py +0 -0
  85. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_bridge.py +0 -0
  86. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_calc.py +0 -0
  87. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_cell_styles.py +0 -0
  88. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_clipboard.py +0 -0
  89. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_control_refs.py +0 -0
  90. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_controls.py +0 -0
  91. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_copy_names.py +0 -0
  92. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_dimensions.py +0 -0
  93. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_duplicates.py +0 -0
  94. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_dynamic_file.py +0 -0
  95. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_editing.py +0 -0
  96. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_engine_book.py +0 -0
  97. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_evaluate.py +0 -0
  98. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_events.py +0 -0
  99. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_find.py +0 -0
  100. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_font_rows.py +0 -0
  101. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_formats.py +0 -0
  102. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_formula_format.py +0 -0
  103. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_hyperlinks.py +0 -0
  104. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_hyperlinks_file.py +0 -0
  105. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_io.py +0 -0
  106. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_merges.py +0 -0
  107. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_model.py +0 -0
  108. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_named_api.py +0 -0
  109. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_names.py +0 -0
  110. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_notes.py +0 -0
  111. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_notes_file.py +0 -0
  112. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_number_format.py +0 -0
  113. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_protection.py +0 -0
  114. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_radios.py +0 -0
  115. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_refresh.py +0 -0
  116. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_region.py +0 -0
  117. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_row_formats.py +0 -0
  118. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_shape_api.py +0 -0
  119. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_shapes.py +0 -0
  120. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_shared.py +0 -0
  121. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_sheet_copy.py +0 -0
  122. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_sheet_move.py +0 -0
  123. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_sort.py +0 -0
  124. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_special_cells.py +0 -0
  125. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_spills.py +0 -0
  126. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_styles.py +0 -0
  127. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_table_edits.py +0 -0
  128. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_tables.py +0 -0
  129. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_text_columns.py +0 -0
  130. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_typing.py +0 -0
  131. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_validation.py +0 -0
  132. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_visible.py +0 -0
  133. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_windows.py +0 -0
  134. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/excel/_worksheet_functions.py +0 -0
  135. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/__init__.py +0 -0
  136. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/_bridge.py +0 -0
  137. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/_io.py +0 -0
  138. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/powerpoint/_model.py +0 -0
  139. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/__init__.py +0 -0
  140. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/_bridge.py +0 -0
  141. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/_io.py +0 -0
  142. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/apps/word/_model.py +0 -0
  143. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/cfb.py +0 -0
  144. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/excel.py +0 -0
  145. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/exceptions.py +0 -0
  146. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/forms.py +0 -0
  147. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/__init__.py +0 -0
  148. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_arity.py +0 -0
  149. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/__init__.py +0 -0
  150. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/catalog.py +0 -0
  151. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/cells.py +0 -0
  152. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/collate.py +0 -0
  153. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/dates.py +0 -0
  154. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/evaluator.py +0 -0
  155. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/__init__.py +0 -0
  156. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/aggregate.py +0 -0
  157. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/arithmetic.py +0 -0
  158. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/arrays.py +0 -0
  159. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/common.py +0 -0
  160. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/conditional.py +0 -0
  161. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/database.py +0 -0
  162. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/dates.py +0 -0
  163. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/distributions.py +0 -0
  164. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/engineering.py +0 -0
  165. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/financial.py +0 -0
  166. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/information.py +0 -0
  167. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/lambdas.py +0 -0
  168. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/logical.py +0 -0
  169. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/lookup.py +0 -0
  170. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/matrix.py +0 -0
  171. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/statistics.py +0 -0
  172. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/subtotal.py +0 -0
  173. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/functions/text.py +0 -0
  174. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/host.py +0 -0
  175. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/lexer.py +0 -0
  176. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/nodes.py +0 -0
  177. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/numbers.py +0 -0
  178. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/parser.py +0 -0
  179. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/precise.py +0 -0
  180. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/reference.py +0 -0
  181. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/registry.py +0 -0
  182. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/special.py +0 -0
  183. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_calc/values.py +0 -0
  184. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_deep.py +0 -0
  185. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_display.py +0 -0
  186. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_formula2.py +0 -0
  187. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_inventory.py +0 -0
  188. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_parse.py +0 -0
  189. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_prefixes.py +0 -0
  190. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_r1c1.py +0 -0
  191. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_spell.py +0 -0
  192. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_structured.py +0 -0
  193. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/formula/_values.py +0 -0
  194. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/__init__.py +0 -0
  195. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_ast.py +0 -0
  196. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_constants_data.py +0 -0
  197. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_intrinsics.py +0 -0
  198. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_inventory.py +0 -0
  199. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_inventory_data.py +0 -0
  200. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_lex.py +0 -0
  201. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_objects.py +0 -0
  202. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_parse.py +0 -0
  203. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_preprocess.py +0 -0
  204. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_runtime.py +0 -0
  205. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_scripting.py +0 -0
  206. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/interpreter/_values.py +0 -0
  207. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/mlang/__init__.py +0 -0
  208. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_eval.py +0 -0
  209. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_inventory.py +0 -0
  210. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_inventory_data.py +0 -0
  211. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_library.py +0 -0
  212. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_parse.py +0 -0
  213. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/mlang/_values.py +0 -0
  214. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerpoint.py +0 -0
  215. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/__init__.py +0 -0
  216. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_binary.py +0 -0
  217. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_files.py +0 -0
  218. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_mashup.py +0 -0
  219. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_metadata.py +0 -0
  220. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_package.py +0 -0
  221. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_refresh.py +0 -0
  222. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_section.py +0 -0
  223. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/_sheets.py +0 -0
  224. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/powerquery/workbook.py +0 -0
  225. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/shapes/__init__.py +0 -0
  226. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_docx.py +0 -0
  227. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_docx_text.py +0 -0
  228. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_drawingml.py +0 -0
  229. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_pptx.py +0 -0
  230. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_values.py +0 -0
  231. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/shapes/_xlsx.py +0 -0
  232. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/vba.py +0 -0
  233. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/vba_pcode.py +0 -0
  234. {pyopenvba-6.3.5 → pyopenvba-6.4.0}/src/pyopenvba/word.py +0 -0
  235. {pyopenvba-6.3.5 → pyopenvba-6.4.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.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,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,15 @@ 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
+ `save()` enforces the following protection and signature gates.
989
997
 
990
998
  ### Password-protected projects
991
999
 
@@ -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,15 @@ 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
+ `save()` enforces the following protection and signature gates.
952
960
 
953
961
  ### Password-protected projects
954
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.5"
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.5"
296
+ __version__ = "6.4.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
@@ -81,7 +81,9 @@ class OpcFile:
81
81
  end = raw.rfind(_END)
82
82
  if end < 0:
83
83
  raise PowerQueryError("this file has no ZIP end record; it is not an Office package")
84
- if raw.rfind(_END64_LOCATOR) >= 0:
84
+ # A ZIP64 locator immediately precedes the end record. Its magic
85
+ # inside a stored part is ordinary content, not an archive marker.
86
+ if raw[max(0, end - 20) : end - 16] == _END64_LOCATOR:
85
87
  raise PowerQueryError("ZIP64 packages are not handled here")
86
88
  count, _size, offset = struct.unpack_from("<HII", raw, end + 10)
87
89
  entries: list[Entry] = []
File without changes
File without changes
File without changes
File without changes
File without changes