pyOpenVBA 6.1.2__tar.gz → 6.1.3__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 (230) hide show
  1. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/PKG-INFO +9 -2
  2. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/README.md +8 -1
  3. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/architecture.md +10 -6
  4. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/ms-ovba-implementation-guide_v2.md +21 -6
  5. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/pyproject.toml +1 -1
  6. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/__init__.py +1 -1
  7. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_host.py +42 -10
  8. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_oforms_pages.py +7 -2
  9. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_oforms_records.py +25 -1
  10. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/cfb.py +33 -15
  11. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/forms.py +389 -110
  12. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/vba.py +33 -8
  13. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/.gitignore +0 -0
  14. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/LICENSE.md +0 -0
  15. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/excel_checklist.csv +0 -0
  16. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/host_completeness.md +0 -0
  17. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/power_query.md +0 -0
  18. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/research/access_complex/README.md +0 -0
  19. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/research/access_designs/README.md +0 -0
  20. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/research/access_macros/README.md +0 -0
  21. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/research/access_write/README.md +0 -0
  22. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/research/pcode/README.md +0 -0
  23. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/roadmap.md +0 -0
  24. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/docs/vba_runtime.md +0 -0
  25. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/__main__.py +0 -0
  26. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_a1.py +0 -0
  27. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_deflate.py +0 -0
  28. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_new_project.py +0 -0
  29. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_package_signature.py +0 -0
  30. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_ppt_container.py +0 -0
  31. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_references.py +0 -0
  32. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/__init__.py +0 -0
  33. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  34. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
  35. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  36. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  37. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  38. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  39. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  40. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
  41. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
  42. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
  43. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/form.blob +0 -0
  44. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
  45. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/form.propdata +0 -0
  46. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
  47. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
  48. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/report.blob +0 -0
  49. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
  50. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/report.propdata +0 -0
  51. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
  52. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
  53. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/_xml.py +0 -0
  54. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/__init__.py +0 -0
  55. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_alloc.py +0 -0
  56. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_btree.py +0 -0
  57. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_collation.py +0 -0
  58. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
  59. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_compact.py +0 -0
  60. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_complex.py +0 -0
  61. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_datapage.py +0 -0
  62. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_ddl.py +0 -0
  63. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_designs.py +0 -0
  64. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_facade.py +0 -0
  65. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_format.py +0 -0
  66. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_index.py +0 -0
  67. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_lval.py +0 -0
  68. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_macros.py +0 -0
  69. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_pages.py +0 -0
  70. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_props.py +0 -0
  71. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_queries.py +0 -0
  72. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_rows.py +0 -0
  73. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_schema.py +0 -0
  74. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_sql.py +0 -0
  75. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_storage.py +0 -0
  76. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_tdef.py +0 -0
  77. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_validate.py +0 -0
  78. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/_vba.py +0 -0
  79. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access/database.py +0 -0
  80. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/access_read.py +0 -0
  81. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/__init__.py +0 -0
  82. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/__init__.py +0 -0
  83. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_arrays.py +0 -0
  84. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_autofill.py +0 -0
  85. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_autofilter.py +0 -0
  86. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_bridge.py +0 -0
  87. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_calc.py +0 -0
  88. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_clipboard.py +0 -0
  89. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_control_refs.py +0 -0
  90. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_controls.py +0 -0
  91. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_copy_names.py +0 -0
  92. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_dimensions.py +0 -0
  93. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_duplicates.py +0 -0
  94. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_dynamic_file.py +0 -0
  95. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_editing.py +0 -0
  96. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_engine_book.py +0 -0
  97. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_evaluate.py +0 -0
  98. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_events.py +0 -0
  99. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_find.py +0 -0
  100. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_font_rows.py +0 -0
  101. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_formats.py +0 -0
  102. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_formula_format.py +0 -0
  103. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_io.py +0 -0
  104. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_merges.py +0 -0
  105. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_model.py +0 -0
  106. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_named_api.py +0 -0
  107. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_names.py +0 -0
  108. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_notes.py +0 -0
  109. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_notes_file.py +0 -0
  110. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_number_format.py +0 -0
  111. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_protection.py +0 -0
  112. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_radios.py +0 -0
  113. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_refresh.py +0 -0
  114. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_region.py +0 -0
  115. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_row_formats.py +0 -0
  116. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_shape_api.py +0 -0
  117. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_shapes.py +0 -0
  118. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_shared.py +0 -0
  119. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_sheet_copy.py +0 -0
  120. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_sheet_move.py +0 -0
  121. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_sort.py +0 -0
  122. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_special_cells.py +0 -0
  123. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_spills.py +0 -0
  124. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_styles.py +0 -0
  125. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_table_edits.py +0 -0
  126. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_tables.py +0 -0
  127. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_typing.py +0 -0
  128. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_validation.py +0 -0
  129. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_visible.py +0 -0
  130. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_windows.py +0 -0
  131. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/excel/_worksheet_functions.py +0 -0
  132. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/powerpoint/__init__.py +0 -0
  133. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/powerpoint/_bridge.py +0 -0
  134. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/powerpoint/_io.py +0 -0
  135. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/powerpoint/_model.py +0 -0
  136. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/word/__init__.py +0 -0
  137. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/word/_bridge.py +0 -0
  138. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/word/_io.py +0 -0
  139. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/apps/word/_model.py +0 -0
  140. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/excel.py +0 -0
  141. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/exceptions.py +0 -0
  142. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/__init__.py +0 -0
  143. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_arity.py +0 -0
  144. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/__init__.py +0 -0
  145. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/catalog.py +0 -0
  146. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/cells.py +0 -0
  147. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/collate.py +0 -0
  148. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/dates.py +0 -0
  149. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/evaluator.py +0 -0
  150. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/__init__.py +0 -0
  151. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/aggregate.py +0 -0
  152. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/arithmetic.py +0 -0
  153. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/arrays.py +0 -0
  154. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/common.py +0 -0
  155. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/conditional.py +0 -0
  156. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/database.py +0 -0
  157. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/dates.py +0 -0
  158. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/distributions.py +0 -0
  159. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/engineering.py +0 -0
  160. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/financial.py +0 -0
  161. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/information.py +0 -0
  162. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/lambdas.py +0 -0
  163. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/logical.py +0 -0
  164. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/lookup.py +0 -0
  165. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/matrix.py +0 -0
  166. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/statistics.py +0 -0
  167. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/subtotal.py +0 -0
  168. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/functions/text.py +0 -0
  169. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/host.py +0 -0
  170. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/lexer.py +0 -0
  171. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/nodes.py +0 -0
  172. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/numbers.py +0 -0
  173. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/parser.py +0 -0
  174. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/precise.py +0 -0
  175. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/reference.py +0 -0
  176. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/registry.py +0 -0
  177. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/special.py +0 -0
  178. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_calc/values.py +0 -0
  179. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_deep.py +0 -0
  180. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_display.py +0 -0
  181. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_formula2.py +0 -0
  182. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_inventory.py +0 -0
  183. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_parse.py +0 -0
  184. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_prefixes.py +0 -0
  185. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_r1c1.py +0 -0
  186. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_spell.py +0 -0
  187. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_structured.py +0 -0
  188. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/formula/_values.py +0 -0
  189. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/__init__.py +0 -0
  190. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_ast.py +0 -0
  191. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_constants_data.py +0 -0
  192. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_intrinsics.py +0 -0
  193. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_inventory.py +0 -0
  194. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_inventory_data.py +0 -0
  195. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_lex.py +0 -0
  196. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_objects.py +0 -0
  197. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_parse.py +0 -0
  198. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_preprocess.py +0 -0
  199. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_runtime.py +0 -0
  200. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_scripting.py +0 -0
  201. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/interpreter/_values.py +0 -0
  202. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/mlang/__init__.py +0 -0
  203. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/mlang/_eval.py +0 -0
  204. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/mlang/_inventory.py +0 -0
  205. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/mlang/_inventory_data.py +0 -0
  206. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/mlang/_library.py +0 -0
  207. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/mlang/_parse.py +0 -0
  208. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/mlang/_values.py +0 -0
  209. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerpoint.py +0 -0
  210. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/__init__.py +0 -0
  211. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/_binary.py +0 -0
  212. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/_files.py +0 -0
  213. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/_mashup.py +0 -0
  214. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/_metadata.py +0 -0
  215. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/_opc.py +0 -0
  216. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/_package.py +0 -0
  217. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/_refresh.py +0 -0
  218. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/_section.py +0 -0
  219. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/_sheets.py +0 -0
  220. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/powerquery/workbook.py +0 -0
  221. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/shapes/__init__.py +0 -0
  222. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/shapes/_docx.py +0 -0
  223. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/shapes/_docx_text.py +0 -0
  224. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/shapes/_drawingml.py +0 -0
  225. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/shapes/_pptx.py +0 -0
  226. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/shapes/_values.py +0 -0
  227. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/shapes/_xlsx.py +0 -0
  228. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/vba_pcode.py +0 -0
  229. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/src/pyopenvba/word.py +0 -0
  230. {pyopenvba-6.1.2 → pyopenvba-6.1.3}/tests/fuzz_corpus/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyOpenVBA
3
- Version: 6.1.2
3
+ Version: 6.1.3
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
@@ -262,6 +262,9 @@ with ExcelFile("workbook.xlsm") as wb:
262
262
  wb.save("out.xlsm")
263
263
  ```
264
264
 
265
+ Deleting a UserForm's module takes the form's design with it, as the
266
+ editor does.
267
+
265
268
  ```python
266
269
  from pyopenvba import AccessDatabase, VBAModuleKind
267
270
 
@@ -380,7 +383,10 @@ None)` clears a property, so the control goes back to its default.
380
383
  MSForms stores a property only when it differs from the control's
381
384
  default, so `properties()` returns what the developer set, which no live
382
385
  host can tell you. Writing is lossless: an unedited form saves back byte
383
- for byte.
386
+ for byte. The stored fields are not always named as VBA names them:
387
+ [docs/userforms.md](docs/userforms.md) says where the designers store a
388
+ property that differs, such as `TextAlign`, and what setting one changes
389
+ besides.
384
390
 
385
391
  The command line shows the tree:
386
392
 
@@ -1049,6 +1055,7 @@ For more:
1049
1055
  - [docs/architecture.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/architecture.md): internal layout and conventions.
1050
1056
  - [docs/access_engine.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/access_engine.md): the Access file format as measured, and what the engine reproduces.
1051
1057
  - [docs/power_query.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/power_query.md): how Excel stores Power Query, and how each rule was measured.
1058
+ - [docs/userforms.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/userforms.md): where the form designers store the UserForm properties VBA names otherwise, as measured.
1052
1059
  - [docs/ms-ovba-implementation-guide_v2.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/ms-ovba-implementation-guide_v2.md): a language-agnostic guide to re-implementing MS-OVBA.
1053
1060
  - [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md): per-feature status.
1054
1061
  - [docs/host_completeness.md](docs/host_completeness.md): whole-host completion criteria and the Excel coverage audit; Excel, then Word, then PowerPoint.
@@ -225,6 +225,9 @@ with ExcelFile("workbook.xlsm") as wb:
225
225
  wb.save("out.xlsm")
226
226
  ```
227
227
 
228
+ Deleting a UserForm's module takes the form's design with it, as the
229
+ editor does.
230
+
228
231
  ```python
229
232
  from pyopenvba import AccessDatabase, VBAModuleKind
230
233
 
@@ -343,7 +346,10 @@ None)` clears a property, so the control goes back to its default.
343
346
  MSForms stores a property only when it differs from the control's
344
347
  default, so `properties()` returns what the developer set, which no live
345
348
  host can tell you. Writing is lossless: an unedited form saves back byte
346
- for byte.
349
+ for byte. The stored fields are not always named as VBA names them:
350
+ [docs/userforms.md](docs/userforms.md) says where the designers store a
351
+ property that differs, such as `TextAlign`, and what setting one changes
352
+ besides.
347
353
 
348
354
  The command line shows the tree:
349
355
 
@@ -1012,6 +1018,7 @@ For more:
1012
1018
  - [docs/architecture.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/architecture.md): internal layout and conventions.
1013
1019
  - [docs/access_engine.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/access_engine.md): the Access file format as measured, and what the engine reproduces.
1014
1020
  - [docs/power_query.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/power_query.md): how Excel stores Power Query, and how each rule was measured.
1021
+ - [docs/userforms.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/userforms.md): where the form designers store the UserForm properties VBA names otherwise, as measured.
1015
1022
  - [docs/ms-ovba-implementation-guide_v2.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/ms-ovba-implementation-guide_v2.md): a language-agnostic guide to re-implementing MS-OVBA.
1016
1023
  - [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md): per-feature status.
1017
1024
  - [docs/host_completeness.md](docs/host_completeness.md): whole-host completion criteria and the Excel coverage audit; Excel, then Word, then PowerPoint.
@@ -345,12 +345,16 @@ written one needs (`scripts/measure_first_macro.py`).
345
345
 
346
346
  ## 5a. UserForm designer streams
347
347
 
348
- A form's design lives beside the VBA storage, not inside it: a root
349
- storage named for the form, holding `f` (the sites: which controls, in
350
- what order), `o` (each control's own property record), and the
351
- `VBFrame` text. Containers nest into storages of their own, named
352
- for the site id -- a `Frame`'s children in `i02`, a `MultiPage`'s Pages
353
- in `i08` / `i09` under its own `i06`.
348
+ A form's design lives beside the VBA storage, not inside it: a storage
349
+ named for the form in the project's storage, holding `f` (the sites:
350
+ which controls, in what order), `o` (each control's own property
351
+ record), and the `VBFrame` text. Containers nest into storages of their
352
+ own, named for the site id -- a `Frame`'s children in `i02`, a
353
+ `MultiPage`'s Pages in `i08` / `i09` under its own `i06`. The project's
354
+ storage is the root of `vbaProject.bin` and of a `.ppt`'s embedded
355
+ project, and `_VBA_PROJECT_CUR` or `Macros` in a `.xls` or `.doc`, which
356
+ hold the document too (`VBAHostFile._project_root`,
357
+ tests/fixtures/binary_forms.json).
354
358
 
355
359
  `forms.py` reads that tree and writes it back; `_oforms_records.py`
356
360
  carries one property table per control class. It never guesses: every
@@ -466,6 +466,7 @@ Document=ThisWorkbook/&H00000000
466
466
  Document=Sheet1/&H00000000
467
467
  Module=Module1
468
468
  Class=Class1
469
+ Package={AC9F2F90-E877-11CE-9F68-00AA00574A4F}
469
470
  BaseClass=UserForm1
470
471
  Name="VBAProject"
471
472
  HelpContextID="0"
@@ -497,6 +498,15 @@ entries together. Do not create `Document=` lines (host-owned). Do
497
498
  not create `BaseClass=` lines unless you also emit a designer
498
499
  sub-storage.
499
500
 
501
+ The editor writes `Package={AC9F2F90-E877-11CE-9F68-00AA00574A4F}`,
502
+ the Forms designer's package, once: immediately before the
503
+ `BaseClass=` line of a form added to a stream that has no such line.
504
+ It never moves or removes the line afterwards, even when the last
505
+ form goes, and a declaration added later goes after it as after any
506
+ other. A project that declares a form without the line gets it only
507
+ with its next new form. Keep every line a mutation does not touch
508
+ byte for byte; a `[Workspace]` value can end in a space.
509
+
500
510
  ---
501
511
 
502
512
  ## 9. The `PROJECTwm` stream
@@ -530,17 +540,20 @@ deprecated control surface.
530
540
 
531
541
  ## 11. UserForms / designer storages
532
542
 
533
- For each `BaseClass=` module there is a **sub-storage** at the CFB root
534
- with the same name as the module. It contains designer streams:
543
+ For each `BaseClass=` module there is a **sub-storage** in the project's
544
+ storage, beside `/VBA`, with the same name as the module: at the CFB
545
+ root of `vbaProject.bin`, in `_VBA_PROJECT_CUR` in a `.xls` and in
546
+ `Macros` in a `.doc`. It contains designer streams:
535
547
  typically `f`, `o`, `\x01CompObj`, `\x03VBFrame`, and sometimes more.
536
548
 
537
549
  Editing the **code-behind** of a UserForm is identical to editing any
538
550
  other module's source: the form's *code* lives in `/VBA/<FormName>` and
539
551
  is round-tripped through the same compressed-source pipeline.
540
552
 
541
- Editing the **layout** (controls, properties, positions) requires the
542
- Office Forms binary format and is out of scope for v1. Preserve the
543
- designer sub-storage byte-for-byte.
553
+ Editing the **layout** (controls, properties, positions) means writing
554
+ the Office Forms binary format, [MS-OFORMS]. pyOpenVBA reads and writes
555
+ it in `forms.py` (docs/architecture.md, section 5a). A writer that does
556
+ not must preserve the designer sub-storage byte for byte.
544
557
 
545
558
  ---
546
559
 
@@ -726,10 +739,12 @@ For each operation, every step must succeed or you must abort.
726
739
  | Drop declaration line | `PROJECT` |
727
740
  | Drop `[Workspace]` entry | `PROJECT` |
728
741
  | Drop NAMEMAP pair | `PROJECTwm` |
742
+ | Remove a form's designer sub-storage | beside `/VBA` |
729
743
  | Invalidate + drop SRP + signatures | as above |
730
744
 
731
745
  Forbidden: deleting `Document=` modules (host-owned), deleting
732
- designer modules without also removing their sub-storage.
746
+ designer modules without also removing their sub-storage. The
747
+ `Package=` line stays when the last form goes.
733
748
 
734
749
  ---
735
750
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyOpenVBA"
7
- version = "6.1.2"
7
+ version = "6.1.3"
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.1.2"
296
+ __version__ = "6.1.3"
@@ -24,7 +24,7 @@ from pyopenvba._new_project import with_project
24
24
  from pyopenvba._package_signature import signature_parts, without_signature
25
25
  from pyopenvba._references import ReferenceManager, module_offset, reference_spans
26
26
  from pyopenvba.exceptions import NoVBAProjectError, UnsupportedFormatError, VBAProjectError
27
- from pyopenvba.forms import VBAForm, create_form, form_names, read_forms
27
+ from pyopenvba.forms import VBAForm, create_form, form_names, read_form, read_forms
28
28
  from pyopenvba.vba import (
29
29
  SignatureInfo,
30
30
  VBAModuleKind,
@@ -226,7 +226,7 @@ class VBAHostFile(ReferenceManager):
226
226
  return {"workbook": "excel", "document": "word", "presentation": "powerpoint"}[self._host_noun]
227
227
 
228
228
  def _reference_forms(self) -> list[str]:
229
- return form_names(self._get_cfb())
229
+ return form_names(self._get_cfb(), root=self._project_root())
230
230
 
231
231
  def references(self) -> list[VBAReference]:
232
232
  """Declared libraries in priority order; none in a file with no project."""
@@ -265,13 +265,14 @@ class VBAHostFile(ReferenceManager):
265
265
  streams do not reconcile.
266
266
 
267
267
  The result is cached, so property edits made on it are the ones
268
- :meth:`save` writes back.
268
+ :meth:`save` writes back. Adding a form and saving keep the same
269
+ objects, and with them the edits made through each.
269
270
  """
270
271
  if not self._has_project:
271
272
  return []
272
273
  if self._forms is None:
273
274
  self._forms = read_forms(
274
- self._get_cfb(), code_page=self.vba_project().code_page
275
+ self._get_cfb(), code_page=self.vba_project().code_page, root=self._project_root()
275
276
  )
276
277
  return self._forms
277
278
 
@@ -294,7 +295,8 @@ class VBAHostFile(ReferenceManager):
294
295
  ``MSForms`` types compiles.
295
296
 
296
297
  The form is returned ready to edit -- ``add_control`` and friends
297
- work on it straight away -- and lands on disk at :meth:`save`.
298
+ work on it straight away -- and lands on disk at :meth:`save`. The
299
+ forms read or added before it are left as they are, edits included.
298
300
  """
299
301
  project = self.vba_project()
300
302
  cfb = self._get_cfb()
@@ -305,13 +307,21 @@ class VBAHostFile(ReferenceManager):
305
307
  width=width,
306
308
  height=height,
307
309
  code_page=project.code_page,
310
+ root=self._project_root(),
308
311
  )
309
312
  project.add_module(name, header, kind=VBAModuleKind.other)
310
313
  # The editor declares Microsoft Forms with a project's first form, and code naming its types needs it.
311
314
  self._ensure_forms_reference()
312
- # The cache was read before this form existed.
313
- self._forms = None
314
- return next(f for f in self.forms() if f.name == name)
315
+ if self._forms is None:
316
+ # Nothing read yet, so nothing edited: reading every form reads this one too.
317
+ return next(f for f in self.forms() if f.name == name)
318
+ # The forms read before keep their objects, and with them any edit made through one and not saved
319
+ # yet: only the new form is read, and the cache keeps the directory order read_forms gives.
320
+ root = self._project_root()
321
+ cached = {form.name: form for form in self._forms}
322
+ cached[name] = read_form(cfb, name, code_page=project.code_page, root=root)
323
+ self._forms = [cached[each] for each in form_names(cfb, root=root)]
324
+ return cached[name]
315
325
 
316
326
  def module_names(self) -> list[str]:
317
327
  """Return the list of VBA module names."""
@@ -630,7 +640,7 @@ class VBAHostFile(ReferenceManager):
630
640
  module.dirty = False
631
641
  if module.kind == VBAModuleKind.standard:
632
642
  decl_key = "Module"
633
- elif module.name in form_names(cfb):
643
+ elif module.name in form_names(cfb, root=self._project_root()):
634
644
  # A designer is declared BaseClass, not Class. The
635
645
  # test is structural -- its name is also a storage
636
646
  # beside VBA/ -- which is the same one forms.py uses
@@ -642,12 +652,22 @@ class VBAHostFile(ReferenceManager):
642
652
  decl_key = "Class"
643
653
  add_modules_for_project.append((module.name, decl_key))
644
654
 
645
- # 3. Delete streams the user removed in-memory.
655
+ # 3. Delete streams the user removed in-memory. A form goes with its
656
+ # designer storage, as the editor removes one; left behind, the
657
+ # storage still reads as a form that holds its name.
658
+ root = self._project_root()
659
+ forms = {name.casefold(): name for name in form_names(cfb, root=root)}
646
660
  for name in sorted(delete_names):
647
661
  try:
648
662
  cfb.remove_stream_in_storage("VBA", name)
649
663
  except KeyError:
650
664
  pass
665
+ if name.casefold() in forms:
666
+ cfb.remove_storage_at(root, forms[name.casefold()])
667
+ if self._forms is not None:
668
+ # Only that form leaves the cache: the others keep their objects, so an edit made
669
+ # through one after this save still saves.
670
+ self._forms = [form for form in self._forms if form.name != forms[name.casefold()]]
651
671
 
652
672
  project.pending_renames.clear()
653
673
  project.pending_adds.clear()
@@ -901,3 +921,15 @@ class VBAHostFile(ReferenceManager):
901
921
  vba_bin = self._zip.read(self._vba_entry)
902
922
  self._cfb = CFB.from_bytes(vba_bin)
903
923
  return self._cfb
924
+
925
+ def _project_root(self) -> tuple[str, ...]:
926
+ """The path of the project's storage in the CFB ``_get_cfb`` returns.
927
+
928
+ Empty where that CFB is the project, as ``vbaProject.bin`` and a
929
+ .ppt's embedded project are. A .xls or .doc holds the document
930
+ too, and Excel and Word keep a form's storage in the project's
931
+ storage beside ``VBA/`` (tests/fixtures/binary_forms.json).
932
+ """
933
+ if self._suffix in self._cfb_formats and self._project_storage is not None:
934
+ return (self._project_storage,)
935
+ return ()
@@ -106,8 +106,13 @@ def serialize_string_array(entries: list[TabString], encoding: str) -> bytes:
106
106
 
107
107
 
108
108
  def new_tab_string(text: str) -> TabString:
109
- """A fresh entry, compressed when every character fits one byte."""
110
- return TabString(text, all(ord(c) <= 0xFF for c in text))
109
+ """A fresh entry, compressed when every character fits one byte.
110
+
111
+ An empty one is stored with no compression flag, a count of zero and
112
+ nothing else, as the designers store a tab's empty tip, tag and
113
+ accelerator (tests/fixtures/form_designer.json).
114
+ """
115
+ return TabString(text, bool(text) and all(ord(c) <= 0xFF for c in text))
111
116
 
112
117
 
113
118
  @dataclass
@@ -69,6 +69,9 @@ class RecordSpec:
69
69
  text_props: bool = False
70
70
  raw_tail: bool = False
71
71
  """Bytes after TextProps (rgColumnInfo, TabStripTabFlags)."""
72
+ flags: tuple[tuple[int, str, bool], ...] = ()
73
+ """Mask bits that store a property with no data: (bit, name, the value
74
+ the bit stands for), the property's default when the bit is clear."""
72
75
 
73
76
 
74
77
  # ---------------------------------------------------------------------------
@@ -153,6 +156,9 @@ COMMAND_BUTTON_SPEC = RecordSpec(
153
156
  extra=(ExtraField(3, "Caption", "str"), ExtraField(5, "Size", "size8")),
154
157
  stream=((7, "Picture"), (10, "MouseIcon")),
155
158
  text_props=True,
159
+ # fTakeFocusOnClick: set when TakeFocusOnClick is False, with no data
160
+ # (tests/fixtures/form_properties.json).
161
+ flags=((9, "TakeFocusOnClick", False),),
156
162
  )
157
163
 
158
164
  LABEL_SPEC = RecordSpec(
@@ -390,6 +396,9 @@ class ParsedRecord:
390
396
  for extra in self.spec.extra:
391
397
  if extra.name == name:
392
398
  return extra.bit
399
+ for bit, flag, _ in self.spec.flags:
400
+ if flag == name:
401
+ return bit
393
402
  return None
394
403
 
395
404
  def properties(self) -> dict[str, object]:
@@ -406,6 +415,13 @@ class ParsedRecord:
406
415
  out[name] = f"<{len(self.pictures[name])} bytes>"
407
416
  else:
408
417
  out[name] = value
418
+ # A string set since the record was read has no length field until
419
+ # it is serialized, but it is stored all the same.
420
+ for name, stored in self.strings.items():
421
+ out.setdefault(name, stored.text)
422
+ for bit, name, value in self.spec.flags:
423
+ if self.mask & (1 << bit):
424
+ out[name] = value
409
425
  out.update(self.sizes)
410
426
  for name, blob in self.arrays.items():
411
427
  out[name] = f"<{len(blob)} bytes>"
@@ -417,7 +433,15 @@ class ParsedRecord:
417
433
  # -- mutation ---------------------------------------------------------
418
434
 
419
435
  def set_value(self, name: str, value: int | None) -> None:
420
- """Set or clear a numeric DataBlock field, adjusting the mask."""
436
+ """Set or clear a numeric DataBlock field, adjusting the mask.
437
+
438
+ A flag, a property the mask stores with no data, is set when
439
+ ``value`` is the one its bit stands for and cleared otherwise.
440
+ """
441
+ flag = next(((bit, when) for bit, flag, when in self.spec.flags if flag == name), None)
442
+ if flag is not None:
443
+ self._set_bit(flag[0], value is not None and bool(value) == flag[1])
444
+ return
421
445
  spec_field = next(
422
446
  (f for f in self.spec.data if f.name == name and f.kind != "marker"),
423
447
  None,
@@ -38,6 +38,11 @@ _FATSECT: int = 0xFFFF_FFFD # sector is part of the FAT
38
38
  _DIFSECT: int = 0xFFFF_FFFC # sector is part of the DIFAT
39
39
  _NOSTREAM: int = 0xFFFF_FFFF # no sibling / no child
40
40
 
41
+ # FAT sector numbers the header lists, and a 512-byte DIFAT sector's after it
42
+ # (the sector's last four bytes hold the next DIFAT sector's number).
43
+ _HEADER_DIFAT = 109
44
+ _DIFAT_PER_SECTOR = 127
45
+
41
46
  # Directory entry object types
42
47
  _OBJTYPE_EMPTY: int = 0
43
48
  _OBJTYPE_STORAGE: int = 1
@@ -836,11 +841,12 @@ class CFB:
836
841
  topology and per-entry metadata (CLSID, state, color, timestamps).
837
842
 
838
843
  Layout, in sector order:
839
- [mini-stream data] [regular stream data] [directory] [mini-FAT] [FAT]
844
+ [mini-stream data] [regular stream data] [directory] [mini-FAT] [FAT] [DIFAT]
840
845
 
841
- Files large enough to require a DIFAT chain (more than 109 FAT sectors,
842
- about 27 MB of total payload) are rejected — vbaProject.bin is always
843
- well under this limit.
846
+ The header lists the first 109 FAT sectors, which cover 109 x 128
847
+ sectors of 512 bytes, about 7.1 MB. A larger file lists the rest in
848
+ DIFAT sectors ([MS-CFB] 2.5): 127 FAT sector numbers each, then the
849
+ number of the next DIFAT sector, the last one ending the chain.
844
850
  """
845
851
  SECTOR = 512
846
852
  MINI = 64
@@ -952,22 +958,22 @@ class CFB:
952
958
  minifat_first = _ENDOFCHAIN
953
959
  n_minifat_sectors = 0
954
960
 
955
- # ---- 4. Decide how many FAT sectors we need (fixed-point) ----
961
+ # ---- 4. Decide how many FAT and DIFAT sectors we need (fixed-point) ----
962
+ # The FAT maps every sector, its own and the DIFAT's included, and the
963
+ # DIFAT lists the FAT sectors the header has no room for.
956
964
  n_data = len(sector_payloads)
957
965
  n_fat = 1
958
966
  while True:
959
- needed = ((n_data + n_fat) + ENTRIES_PER_SECTOR - 1) // ENTRIES_PER_SECTOR
967
+ n_difat = -(-max(0, n_fat - _HEADER_DIFAT) // _DIFAT_PER_SECTOR)
968
+ needed = -(-(n_data + n_fat + n_difat) // ENTRIES_PER_SECTOR)
960
969
  if needed <= n_fat:
961
970
  break
962
971
  n_fat = needed
963
- if n_fat > 109:
964
- raise CFBError(
965
- "CFB writer requires DIFAT chain support for files this large; "
966
- "not implemented."
967
- )
968
972
 
969
973
  fat_first = n_data
970
974
  fat_sector_ids = list(range(fat_first, fat_first + n_fat))
975
+ difat_first = fat_first + n_fat
976
+ difat_sector_ids = list(range(difat_first, difat_first + n_difat))
971
977
 
972
978
  # ---- 5. Build the FAT ----
973
979
  fat = [_FREESECT] * (n_fat * ENTRIES_PER_SECTOR)
@@ -989,14 +995,26 @@ class CFB:
989
995
  chain(minifat_first, n_minifat_sectors)
990
996
  for s in fat_sector_ids:
991
997
  fat[s] = _FATSECT
998
+ for s in difat_sector_ids:
999
+ fat[s] = _DIFSECT
992
1000
 
993
1001
  fat_bytes = struct.pack(f"<{len(fat)}I", *fat)
994
1002
  for k in range(n_fat):
995
1003
  sector_payloads.append(fat_bytes[k * SECTOR:(k + 1) * SECTOR])
996
1004
 
1005
+ # The FAT sectors past the header's 109, 127 to a DIFAT sector, each
1006
+ # sector ending with the next one's number.
1007
+ rest = fat_sector_ids[_HEADER_DIFAT:]
1008
+ for k in range(n_difat):
1009
+ listed = rest[k * _DIFAT_PER_SECTOR:(k + 1) * _DIFAT_PER_SECTOR]
1010
+ listed += [_FREESECT] * (_DIFAT_PER_SECTOR - len(listed))
1011
+ following = difat_sector_ids[k + 1] if k + 1 < n_difat else _ENDOFCHAIN
1012
+ sector_payloads.append(struct.pack(f"<{_DIFAT_PER_SECTOR + 1}I", *listed, following))
1013
+
997
1014
  # ---- 6. Build the header ----
998
- difat = list(fat_sector_ids) + [_FREESECT] * (109 - len(fat_sector_ids))
999
- difat_bytes = struct.pack("<109I", *difat)
1015
+ difat = fat_sector_ids[:_HEADER_DIFAT]
1016
+ difat += [_FREESECT] * (_HEADER_DIFAT - len(difat))
1017
+ difat_bytes = struct.pack(f"<{_HEADER_DIFAT}I", *difat)
1000
1018
 
1001
1019
  header = struct.pack(
1002
1020
  _HEADER_FMT,
@@ -1015,8 +1033,8 @@ class CFB:
1015
1033
  CUTOFF, # mini-stream cutoff size
1016
1034
  minifat_first, # first mini-FAT sector
1017
1035
  n_minifat_sectors, # num mini-FAT sectors
1018
- _ENDOFCHAIN, # first DIFAT sector (none)
1019
- 0, # num DIFAT sectors
1036
+ difat_first if n_difat else _ENDOFCHAIN, # first DIFAT sector
1037
+ n_difat, # num DIFAT sectors
1020
1038
  difat_bytes, # 436-byte DIFAT array
1021
1039
  )
1022
1040