pyOpenVBA 6.2.0__tar.gz → 6.3.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 (237) hide show
  1. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/PKG-INFO +13 -1
  2. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/README.md +12 -0
  3. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/architecture.md +11 -9
  4. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/research/access_write/README.md +87 -3
  5. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/pyproject.toml +1 -1
  6. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/__init__.py +1 -1
  7. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_designs.py +131 -8
  8. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_macros.py +1 -3
  9. pyopenvba-6.3.0/src/pyopenvba/access/_storage.py +238 -0
  10. pyopenvba-6.3.0/src/pyopenvba/access/_themes.py +200 -0
  11. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_vba.py +200 -20
  12. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/database.py +398 -135
  13. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access_read.py +253 -731
  14. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/vba.py +38 -73
  15. pyopenvba-6.2.0/src/pyopenvba/_templates/designs/form.propdata +0 -0
  16. pyopenvba-6.2.0/src/pyopenvba/_templates/designs/report.propdata +0 -0
  17. pyopenvba-6.2.0/src/pyopenvba/access/_storage.py +0 -119
  18. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/.gitignore +0 -0
  19. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/LICENSE.md +0 -0
  20. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/excel_checklist.csv +0 -0
  21. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/host_completeness.md +0 -0
  22. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  23. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/power_query.md +0 -0
  24. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/research/access_complex/README.md +0 -0
  25. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/research/access_designs/README.md +0 -0
  26. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/research/access_macros/README.md +0 -0
  27. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/research/pcode/README.md +0 -0
  28. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/roadmap.md +0 -0
  29. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/docs/vba_runtime.md +0 -0
  30. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/__main__.py +0 -0
  31. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_a1.py +0 -0
  32. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_deflate.py +0 -0
  33. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_host.py +0 -0
  34. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_new_project.py +0 -0
  35. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_oforms_pages.py +0 -0
  36. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_oforms_records.py +0 -0
  37. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_package_signature.py +0 -0
  38. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_ppt_container.py +0 -0
  39. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_references.py +0 -0
  40. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_relationships.py +0 -0
  41. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/__init__.py +0 -0
  42. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  43. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
  44. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  45. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  46. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  47. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  48. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  49. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
  50. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
  51. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
  52. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/designs/form.blob +0 -0
  53. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
  54. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
  55. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
  56. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/designs/report.blob +0 -0
  57. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
  58. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
  59. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
  60. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/_xml.py +0 -0
  61. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/__init__.py +0 -0
  62. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_alloc.py +0 -0
  63. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_btree.py +0 -0
  64. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_collation.py +0 -0
  65. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
  66. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_compact.py +0 -0
  67. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_complex.py +0 -0
  68. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_datapage.py +0 -0
  69. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_ddl.py +0 -0
  70. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_facade.py +0 -0
  71. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_format.py +0 -0
  72. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_index.py +0 -0
  73. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_lval.py +0 -0
  74. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_pages.py +0 -0
  75. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_props.py +0 -0
  76. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_queries.py +0 -0
  77. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_rows.py +0 -0
  78. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_schema.py +0 -0
  79. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_sql.py +0 -0
  80. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_tdef.py +0 -0
  81. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/access/_validate.py +0 -0
  82. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/__init__.py +0 -0
  83. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/__init__.py +0 -0
  84. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_arrays.py +0 -0
  85. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_autofill.py +0 -0
  86. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_autofilter.py +0 -0
  87. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_bridge.py +0 -0
  88. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_calc.py +0 -0
  89. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_cell_styles.py +0 -0
  90. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_clipboard.py +0 -0
  91. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_control_refs.py +0 -0
  92. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_controls.py +0 -0
  93. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_copy_names.py +0 -0
  94. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_dimensions.py +0 -0
  95. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_duplicates.py +0 -0
  96. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_dynamic_file.py +0 -0
  97. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_editing.py +0 -0
  98. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_engine_book.py +0 -0
  99. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_evaluate.py +0 -0
  100. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_events.py +0 -0
  101. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_find.py +0 -0
  102. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_font_rows.py +0 -0
  103. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_formats.py +0 -0
  104. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_formula_format.py +0 -0
  105. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_hyperlinks.py +0 -0
  106. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_hyperlinks_file.py +0 -0
  107. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_io.py +0 -0
  108. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_merges.py +0 -0
  109. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_model.py +0 -0
  110. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_named_api.py +0 -0
  111. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_names.py +0 -0
  112. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_notes.py +0 -0
  113. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_notes_file.py +0 -0
  114. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_number_format.py +0 -0
  115. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_protection.py +0 -0
  116. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_radios.py +0 -0
  117. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_refresh.py +0 -0
  118. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_region.py +0 -0
  119. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_row_formats.py +0 -0
  120. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_shape_api.py +0 -0
  121. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_shapes.py +0 -0
  122. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_shared.py +0 -0
  123. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_sheet_copy.py +0 -0
  124. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_sheet_move.py +0 -0
  125. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_sort.py +0 -0
  126. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_special_cells.py +0 -0
  127. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_spills.py +0 -0
  128. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_styles.py +0 -0
  129. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_table_edits.py +0 -0
  130. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_tables.py +0 -0
  131. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_text_columns.py +0 -0
  132. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_typing.py +0 -0
  133. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_validation.py +0 -0
  134. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_visible.py +0 -0
  135. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_windows.py +0 -0
  136. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/excel/_worksheet_functions.py +0 -0
  137. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/powerpoint/__init__.py +0 -0
  138. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/powerpoint/_bridge.py +0 -0
  139. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/powerpoint/_io.py +0 -0
  140. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/powerpoint/_model.py +0 -0
  141. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/word/__init__.py +0 -0
  142. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/word/_bridge.py +0 -0
  143. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/word/_io.py +0 -0
  144. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/apps/word/_model.py +0 -0
  145. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/cfb.py +0 -0
  146. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/excel.py +0 -0
  147. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/exceptions.py +0 -0
  148. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/forms.py +0 -0
  149. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/__init__.py +0 -0
  150. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_arity.py +0 -0
  151. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/__init__.py +0 -0
  152. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/catalog.py +0 -0
  153. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/cells.py +0 -0
  154. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/collate.py +0 -0
  155. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/dates.py +0 -0
  156. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/evaluator.py +0 -0
  157. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/__init__.py +0 -0
  158. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/aggregate.py +0 -0
  159. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/arithmetic.py +0 -0
  160. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/arrays.py +0 -0
  161. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/common.py +0 -0
  162. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/conditional.py +0 -0
  163. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/database.py +0 -0
  164. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/dates.py +0 -0
  165. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/distributions.py +0 -0
  166. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/engineering.py +0 -0
  167. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/financial.py +0 -0
  168. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/information.py +0 -0
  169. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/lambdas.py +0 -0
  170. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/logical.py +0 -0
  171. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/lookup.py +0 -0
  172. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/matrix.py +0 -0
  173. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/statistics.py +0 -0
  174. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/subtotal.py +0 -0
  175. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/functions/text.py +0 -0
  176. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/host.py +0 -0
  177. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/lexer.py +0 -0
  178. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/nodes.py +0 -0
  179. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/numbers.py +0 -0
  180. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/parser.py +0 -0
  181. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/precise.py +0 -0
  182. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/reference.py +0 -0
  183. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/registry.py +0 -0
  184. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/special.py +0 -0
  185. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_calc/values.py +0 -0
  186. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_deep.py +0 -0
  187. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_display.py +0 -0
  188. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_formula2.py +0 -0
  189. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_inventory.py +0 -0
  190. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_parse.py +0 -0
  191. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_prefixes.py +0 -0
  192. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_r1c1.py +0 -0
  193. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_spell.py +0 -0
  194. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_structured.py +0 -0
  195. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/formula/_values.py +0 -0
  196. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/__init__.py +0 -0
  197. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_ast.py +0 -0
  198. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_constants_data.py +0 -0
  199. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_intrinsics.py +0 -0
  200. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_inventory.py +0 -0
  201. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_inventory_data.py +0 -0
  202. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_lex.py +0 -0
  203. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_objects.py +0 -0
  204. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_parse.py +0 -0
  205. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_preprocess.py +0 -0
  206. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_runtime.py +0 -0
  207. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_scripting.py +0 -0
  208. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/interpreter/_values.py +0 -0
  209. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/mlang/__init__.py +0 -0
  210. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/mlang/_eval.py +0 -0
  211. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/mlang/_inventory.py +0 -0
  212. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/mlang/_inventory_data.py +0 -0
  213. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/mlang/_library.py +0 -0
  214. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/mlang/_parse.py +0 -0
  215. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/mlang/_values.py +0 -0
  216. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerpoint.py +0 -0
  217. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/__init__.py +0 -0
  218. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/_binary.py +0 -0
  219. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/_files.py +0 -0
  220. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/_mashup.py +0 -0
  221. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/_metadata.py +0 -0
  222. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/_opc.py +0 -0
  223. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/_package.py +0 -0
  224. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/_refresh.py +0 -0
  225. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/_section.py +0 -0
  226. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/_sheets.py +0 -0
  227. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/powerquery/workbook.py +0 -0
  228. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/shapes/__init__.py +0 -0
  229. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/shapes/_docx.py +0 -0
  230. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/shapes/_docx_text.py +0 -0
  231. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/shapes/_drawingml.py +0 -0
  232. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/shapes/_pptx.py +0 -0
  233. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/shapes/_values.py +0 -0
  234. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/shapes/_xlsx.py +0 -0
  235. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/vba_pcode.py +0 -0
  236. {pyopenvba-6.2.0 → pyopenvba-6.3.0}/src/pyopenvba/word.py +0 -0
  237. {pyopenvba-6.2.0 → pyopenvba-6.3.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.2.0
3
+ Version: 6.3.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
@@ -936,6 +936,18 @@ A project its application would not write is not written here either:
936
936
  Excel writes only the code names for one with no code in it, and
937
937
  PowerPoint nothing for one with no module.
938
938
 
939
+ An Access database answers the same way until its first module, since
940
+ Access makes no project for a database that has never held code.
941
+ `AccessDatabase.add_vba_project()` gives it the one Access makes:
942
+
943
+ ```python
944
+ with AccessDatabase("orders.accdb") as db:
945
+ if not db.has_vba_project():
946
+ db.add_vba_project()
947
+ db.add_module("Module1", "Public Function Ping() As Long\r\n Ping = 1\r\nEnd Function")
948
+ db.save()
949
+ ```
950
+
939
951
  ### Access
940
952
 
941
953
  | Extension | What it is | Read | Write | create_new |
@@ -899,6 +899,18 @@ A project its application would not write is not written here either:
899
899
  Excel writes only the code names for one with no code in it, and
900
900
  PowerPoint nothing for one with no module.
901
901
 
902
+ An Access database answers the same way until its first module, since
903
+ Access makes no project for a database that has never held code.
904
+ `AccessDatabase.add_vba_project()` gives it the one Access makes:
905
+
906
+ ```python
907
+ with AccessDatabase("orders.accdb") as db:
908
+ if not db.has_vba_project():
909
+ db.add_vba_project()
910
+ db.add_module("Module1", "Public Function Ping() As Long\r\n Ping = 1\r\nEnd Function")
911
+ db.save()
912
+ ```
913
+
902
914
  ### Access
903
915
 
904
916
  | Extension | What it is | Read | Write | create_new |
@@ -230,7 +230,8 @@ follows and how it was measured is in
230
230
  [docs/access_engine.md](access_engine.md).
231
231
 
232
232
  `AccessDatabase` exposes the same surface as the other hosts:
233
- `module_names()`, `get_module()`, `set_module()`, `vba_project()` with
233
+ `has_vba_project()` and `add_vba_project()` for a database that has
234
+ never held code, `module_names()`, `get_module()`, `set_module()`, `vba_project()` with
234
235
  `add_module` / `rename_module` / `delete_module`, `pull_modules()` /
235
236
  `push_modules()`, `forms()` and `add_form()` with `add_control` /
236
237
  `remove_control` / `set_property` on the result, `save()`, `create_new()`.
@@ -246,11 +247,12 @@ the project from the module streams on the next open, as Access's own
246
247
  `/decompile` does. The code has to compile; a live gate has Access run
247
248
  what was written and compares the value the code returns.
248
249
 
249
- `AccessReader` predates the engine: a page scanner that finds the MS-OVBA
250
- blobs by signature and decodes `MSysObjects` on its own. It keeps its
251
- read API (`get_module`, `vba_modules`, `read_project_info`, `identifiers`,
252
- `disassemble_module`, the catalog readers, `pull_modules`) and
253
- `pull_access` still uses it; it writes nothing. The chronicle of the
250
+ `AccessReader` predates the engine and now reads through it: modules,
251
+ the project's streams and `MSysObjects` come from the rows the engine
252
+ finds them in, and text is decoded in the project's code page. It keeps
253
+ its read API (`get_module`, `vba_modules`, `read_project_info`,
254
+ `identifiers`, `disassemble_module`, the catalog readers,
255
+ `pull_modules`) and `pull_access` still uses it; it writes nothing. The chronicle of the
254
256
  attempts that came before the engine is
255
257
  [docs/msaccess_lessons_learned.md](msaccess_lessons_learned.md).
256
258
 
@@ -488,9 +490,9 @@ tests/
488
490
  test_powerpoint.py PowerPointFile facade, end-to-end
489
491
  test_pull_push.py Disk workflow
490
492
  test_gates.py Per-roadmap-gate regression tests
491
- test_access.py AccessReader read path: page walk,
492
- LVAL chain walk, MS-OVBA blob decode,
493
- byte-for-byte oracle parity (EXPERIMENTAL)
493
+ test_access.py AccessReader read path: modules through
494
+ MSysAccessStorage, code pages,
495
+ byte-for-byte oracle parity
494
496
  test_access_engine.py Storage engine read layer against the
495
497
  Access-authored fixtures
496
498
  test_live_access_engine_gate.py
@@ -720,11 +720,18 @@ What it writes:
720
720
  `Modules`, a 13-byte `PropData` under it, and the module's stream under
721
721
  `VBA` with a row name of 28 random capitals
722
722
  * an entry in `Modules/PropData`, the list of those folders:
723
- `05 09 02 <folder name, one UTF-16 character> "CB0"`, eleven bytes each
723
+ `05 09 02 <folder name, one UTF-16 character> "CB0"`, eleven bytes each.
724
+ The `09` and `02` are sizes, of the rest of the entry and of the name,
725
+ so a two-digit folder is `05 0b 04 "10" "CB0"` (GitHub issue #34).
726
+ *2026-09-27:* Access writes this entry, and the module's
727
+ `MSysNavPaneGroupToObjects` row, the next time it opens the database,
728
+ not when it adds the module, so the library now leaves both to it
729
+ (`tests/live_access_test/first_module_reopened.accdb`)
724
730
  * a dir block of eleven records and PROJECTMODULES up by one
725
731
  * entries in DirData, PROJECTwm and PROJECT
726
732
  * an `MSysObjects` row of type -32761, a `MSysNavPaneObjectIDs` row and a
727
- `MSysNavPaneGroupToObjects` row in group 8
733
+ `MSysNavPaneGroupToObjects` row in group 8 (the last, since 2026-09-27,
734
+ left to Access, which adds it on its next open)
728
735
  * the `Version` word of `_VBA_PROJECT` set to a value VBA does not know
729
736
 
730
737
  It works on the shipped blank template too, which an earlier note had
@@ -733,6 +740,20 @@ all it needed.
733
740
 
734
741
  ### Three allocations that are computed, not chosen
735
742
 
743
+ > **Corrected 2026-09-27.** The folder base of `4` and the id step of four
744
+ > below were measured with Access driven through pyvbaharness, which
745
+ > injects three modules of its own (`PyVbaHarnessRunner`,
746
+ > `PyVbaHarnessCall`, `PyVbaUserCode`) into the project it runs code in.
747
+ > While they exist they hold folders `1`, `2`, `3` and the next three ids,
748
+ > so the module Access added took `4` and an id four higher; the `{0, 5}`
749
+ > case reusing `4` is the same effect. Access driven over COM alone gives
750
+ > the lowest free folder counting from `0` and the next id
751
+ > (`tests/live_access_test/modules_added.accdb`), the library now does
752
+ > the same, and the live gate lists and runs modules in folders `1` and
753
+ > up. The `AllModules(i).Name` failures that seemed to confirm the old
754
+ > rules came from `\x03DirData` naming folder `4` for every module, which
755
+ > was fixed the same day.
756
+
736
757
  Both were found the same way -- a created module that the VBE listed and
737
758
  ran, while `CurrentProject.AllModules(i).Name` failed with "refers to an
738
759
  object that is closed or doesn't exist".
@@ -743,7 +764,10 @@ not folders>)`. `Modules` holds four such rows -- `PropData`,
743
764
  `PropDataCopy`, `DirData` and `DirDataCopy` -- so its folders start at
744
765
  `4`, which is why the second module in a database gets `4` and never `1`.
745
766
  Only the computed name works: on the blank template `1`, `9` and `A` all
746
- fail while `4` succeeds.
767
+ fail while `4` succeeds. Past `9` the name is the number in decimal,
768
+ `10`, not the character after `9`: Access filled every container of the
769
+ template past folder 9 and named them `10`, `11` and `12` (GitHub issue
770
+ #34, `tests/live_access_test/folders_past_nine.accdb`).
747
771
 
748
772
  Two rules fit the first four measurements and only one fits all six. A
749
773
  count, `chr(0x30 + children - 1)`, works while folders stay contiguous.
@@ -765,6 +789,66 @@ next id: the step is what an object reserves, not a global stride.
765
789
 
766
790
  That last line is the whole trick, and it is why the list is short.
767
791
 
792
+ ### The order of `\x03DirData` and `PropData`: an `std::unordered_map`
793
+
794
+ Access does not append a new object's entry. Adding `Macro1` to
795
+ `Macro11` gave `Macro1`, `Macro10`, `Macro2`, `Macro11`, `Macro3` and on
796
+ to `Macro9`, in one session or in eleven. Forms named `Macro1` to
797
+ `Macro11` come out the same way and macros named `Form1` to `Form11` in
798
+ order, so the order follows the names. Adding `Mod2` to `Mod13` one
799
+ session each, with a copy of the file after every session, showed
800
+ entries that the session never touched changing places:
801
+
802
+ ```
803
+ after Mod5 Module1 Mod2 Mod3 Mod4 Mod5
804
+ after Mod6 Mod6 Module1 Mod2 Mod3 Mod4 Mod5
805
+ after Mod7 Module1 Mod6 Mod2 Mod3 Mod4 Mod5 Mod7
806
+ after Mod11 Module1 Mod6 Mod11 Mod2 Mod3 Mod4 Mod5 Mod7 Mod8 Mod9 Mod10
807
+ after Mod13 Module1 Mod6 Mod11 Mod2 Mod12 Mod3 Mod13 Mod4 Mod5 Mod7 Mod8 Mod9 Mod10
808
+ ```
809
+
810
+ Guessing the hash from orders like these went nowhere, so the answer
811
+ came from `MSACCESS.EXE`. The storage names sit in a table beside a
812
+ stream class whose reader inserts each entry it reads into a container
813
+ that guards itself with "unordered_map/set too long". A container's
814
+ `\x03DirData` is an MSVC `std::unordered_map` keyed by object name, its
815
+ `PropData` folder list another keyed by folder name, and each stream is
816
+ the map's own order. From the code, and checked against every probe:
817
+
818
+ - **The hash** (at `0x14006c060`) is 16 bits: for each character that
819
+ counts, `h = (h << 5) + (h >> 13) + 1 + (c & 0x1f)`. So case does not
820
+ matter and `0` hashes as `P`. A byte table decides what counts among
821
+ ASCII characters: `"`, `'`, `~` and control characters are skipped,
822
+ and a space or tab counts as 0. A leading `.` is skipped. From the
823
+ first character past ASCII, the rest of the name goes through the
824
+ system code page (`WideCharToMultiByte`, no best fit) and
825
+ `LCMapStringA` upper-casing, a byte at a time, every byte above 1
826
+ counting. A Turkish UI turns two bytes into `I`, which is not
827
+ reproduced.
828
+ - **The map** starts with 8 buckets; a key's bucket is `hash & (buckets
829
+ - 1)`. A new key goes in front of the first key of its bucket, or at
830
+ the end when its bucket is empty. An insert that would leave more keys
831
+ than buckets first grows the map to a power of two at least the new
832
+ size, and eightfold while it is under 512 buckets. The rehash walks the
833
+ list and moves each key to the front of its new bucket.
834
+ - **Loading** inserts the stored entries in their order. That is why a
835
+ session reverses some pairs and not others: the ninth entry grows the
836
+ map, so the first eight are reversed twice and the rest once.
837
+ - **Writing** happens only when the map changed. A delete erases; a
838
+ rename erases and inserts, in both lists under `DoCmd.Rename`, while a
839
+ module renamed in the VBE leaves `PropData` alone. A new object's
840
+ `PropData` line waits for the next open, when the missing lines are
841
+ inserted in DirData's order.
842
+
843
+ Checked on 13 histories, every snapshot included: 36 one-character names
844
+ made in one session forwards and backwards and then reopened, `Mod2` to
845
+ `Mod13` one session each, `Macro1` to `Macro11` both ways, the forms and
846
+ reports, `modules_added.accdb` and three small fixtures. Also on a
847
+ delete, two renames, seven names past ASCII and four with skipped
848
+ characters, and on `_container_order.ps1`'s delete and rename of
849
+ modules, a form and a report in copies of `folders_past_nine.accdb`.
850
+ The library writes these orders (`_storage.access_order`).
851
+
768
852
  ### `_VBA_PROJECT` is a cache, so invalidate it instead of forging it
769
853
 
770
854
  *This is what shipped; see `pyopenvba/access/_vba.py`.*
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyOpenVBA"
7
- version = "6.2.0"
7
+ version = "6.3.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.2.0"
296
+ __version__ = "6.3.0"
@@ -35,10 +35,11 @@ import re
35
35
  import struct
36
36
  import unicodedata
37
37
  from collections.abc import Iterable, Mapping, Sequence
38
- from dataclasses import dataclass, field
38
+ from dataclasses import dataclass, field, replace
39
39
  from pathlib import Path
40
40
  from typing import NamedTuple
41
41
 
42
+ from pyopenvba.access._themes import OFFICE_2007, Theme, theme_color
42
43
  from pyopenvba.access_read import AccessError
43
44
 
44
45
  #: `<u32 id><u16 code><u32 type><u32 width><u32 length>`.
@@ -107,6 +108,11 @@ NAME_CODES = (20, 21)
107
108
 
108
109
  #: Where the captured designs live, one folder stream each.
109
110
  TEMPLATES = Path(__file__).parents[1] / "_templates" / "designs"
111
+ #: A new form's or report's own `PropData` as Access writes it when it
112
+ #: makes one: property 1, the flag `set_design_code` sets, and nothing
113
+ #: else. A design saved again in a later session gains property 0 as
114
+ #: well, which is how the captured designs carried it.
115
+ NEW_DESIGN_PROP_DATA = bytes.fromhex("00000000" "02" "01000000" "00000000")
110
116
  #: The record that carries a design's GUID, which the catalog row repeats.
111
117
  GUID_RECORD = 208
112
118
  GUID_LENGTH = 16
@@ -2180,21 +2186,134 @@ _PROTOTYPES: dict[str, dict[int, tuple[DesignRecord, ...]]] = {}
2180
2186
  DEFAULTS_FREE = frozenset({118})
2181
2187
 
2182
2188
 
2183
- def prototype_records(kind: str, type_code: int) -> tuple[DesignRecord, ...]:
2189
+ def prototype_records(kind: str, type_code: int, theme: Theme = OFFICE_2007) -> tuple[DesignRecord, ...]:
2184
2190
  """The records of the control-defaults object for `type_code` on a
2185
- design of `kind`; refused for a type the fixture did not hold."""
2191
+ design of `kind`, as `theme` gives them; refused for a type the
2192
+ fixture did not hold."""
2186
2193
  if kind not in _PROTOTYPES:
2187
2194
  _header, objects, _trailer = parse_design(template(kind, "prototypes"))
2188
2195
  _PROTOTYPES[kind] = {o.type: o.records for o in objects[1:] if o.type is not None}
2189
2196
  try:
2190
- return _PROTOTYPES[kind][type_code]
2197
+ records = _PROTOTYPES[kind][type_code]
2191
2198
  except KeyError:
2192
2199
  name = CONTROL_TYPES.get(type_code, type_code)
2193
2200
  raise AccessError(f"no control defaults were captured for a {name} on a {kind}") from None
2201
+ type_name = CONTROL_TYPES.get(type_code, "")
2202
+ measured = PROPERTY_SLOTS.get(type_name, {}).get("TextFontFamily")
2203
+ return rethemed(records, theme, measured[0] if measured else FAMILY_IDS.get(type_name))
2204
+
2205
+
2206
+ # --- what a design takes from its database's theme -------------------------------
2207
+ # The templates and the control defaults were captured in a database on
2208
+ # Office's 2007 theme. Access builds the same objects from whatever theme
2209
+ # the database holds (`_themes`), and the differences are these, measured
2210
+ # by having Access make one form and one report with a control of every
2211
+ # type in a database on each Office theme:
2212
+ #
2213
+ # * a font comes from the theme by its ThemeFontIndex, 0 the heading face
2214
+ # and 1 the body face, with the face's family byte beside it
2215
+ # (TextFontFamily) unless it is the default a sans face takes;
2216
+ # * a colour is worked out again from the slot, tint and shade beside it;
2217
+ # * the design itself carries the body face, its family byte and the
2218
+ # theme's Background 2 colour.
2219
+ #
2220
+ # Access also sizes a tab control's strip and pages by the face's metrics,
2221
+ # which is not reproduced here.
2222
+
2223
+ #: A defaults object's family-byte slot where the measured slots have none.
2224
+ FAMILY_IDS = {"NavigationButton": 368}
2225
+ THEME_FONT_INDEX = PROPERTY_CODES["ThemeFontIndex"]
2226
+ FONT_NAME = PROPERTY_CODES["FontName"]
2227
+ TEXT_FONT_FAMILY = PROPERTY_CODES["TextFontFamily"]
2228
+ #: The design's own face, its family byte's slot, and its Background 2.
2229
+ DESIGN_FONT = 160
2230
+ DESIGN_FONT_FAMILY_SLOT = (56, 244, 2, 1)
2231
+ DESIGN_BACKGROUND = 319
2232
+ BACKGROUND_2 = 3
2233
+ #: Each themed colour beside the slot, tint and shade it is worked out from.
2234
+ THEMED_COLORS = tuple(
2235
+ (
2236
+ PROPERTY_CODES[f"{part}Color"],
2237
+ PROPERTY_CODES[f"{part}ThemeColorIndex"],
2238
+ PROPERTY_CODES.get(f"{part}Tint"),
2239
+ PROPERTY_CODES.get(f"{part}Shade"),
2240
+ )
2241
+ for part in ("Back", "Border", "Fore", "Gridline", "Hover", "Pressed", "HoverFore", "PressedFore")
2242
+ )
2243
+
2244
+
2245
+ def _signed(record: DesignRecord) -> int:
2246
+ return int.from_bytes(record.value, "little", signed=True)
2247
+
2248
+
2249
+ def _themed_face(
2250
+ out: dict[int, DesignRecord], face: str, family: DesignRecord | None, family_slot: tuple[int, ...] | None
2251
+ ) -> None:
2252
+ """Set the family byte `face` takes: none for the default, else the
2253
+ record at `family_slot`, `(id, code, value type, width)`."""
2254
+ byte = FONT_FAMILIES.get(face)
2255
+ if byte is None:
2256
+ if family is not None:
2257
+ del out[family.id]
2258
+ elif family is not None:
2259
+ out[family.id] = replace(family, value=bytes((byte,)))
2260
+ elif family_slot is not None:
2261
+ slot_id, code, value_type, width = family_slot[:4]
2262
+ out[slot_id] = DesignRecord(slot_id, code, value_type, width, bytes((byte,)))
2263
+
2264
+
2265
+ def rethemed(records: Sequence[DesignRecord], theme: Theme, family_id: int | None) -> tuple[DesignRecord, ...]:
2266
+ """Records captured on Office's 2007 theme, as `theme` gives them.
2267
+ `family_id` is where the object's family byte goes when the theme's
2268
+ face needs one and the object has none."""
2269
+ if theme == OFFICE_2007:
2270
+ return tuple(records)
2271
+ by_code = {r.code: r for r in records}
2272
+ out = {r.id: r for r in records}
2273
+ index = by_code.get(THEME_FONT_INDEX)
2274
+ font = by_code.get(FONT_NAME)
2275
+ if index is not None and font is not None and _signed(index) in (0, 1):
2276
+ face = theme.major_font if _signed(index) == 0 else theme.minor_font
2277
+ out[font.id] = replace(font, value=face.encode("utf-16-le"))
2278
+ slot = None if family_id is None else (family_id, TEXT_FONT_FAMILY, 2, 1)
2279
+ _themed_face(out, face, by_code.get(TEXT_FONT_FAMILY), slot)
2280
+ for color, slot_code, tint_code, shade_code in THEMED_COLORS:
2281
+ found, slot_record = by_code.get(color), by_code.get(slot_code)
2282
+ if found is None or slot_record is None or _signed(slot_record) < 0:
2283
+ continue
2284
+ tint = by_code.get(tint_code) if tint_code is not None else None
2285
+ shade = by_code.get(shade_code) if shade_code is not None else None
2286
+ out[found.id] = replace(found, value=theme_color(
2287
+ theme,
2288
+ _signed(slot_record),
2289
+ struct.unpack("<f", tint.value)[0] if tint is not None else 100.0,
2290
+ struct.unpack("<f", shade.value)[0] if shade is not None else 100.0,
2291
+ ))
2292
+ return tuple(out[record_id] for record_id in sorted(out))
2293
+
2294
+
2295
+ def rethemed_design(blob: bytes, theme: Theme) -> bytes:
2296
+ """An empty design from the templates, as `theme` gives it."""
2297
+ if theme == OFFICE_2007:
2298
+ return blob
2299
+ header, objects, trailer = parse_design(blob)
2300
+ own = {r.id: r for r in objects[0].records}
2301
+ by_code = {r.code: r for r in objects[0].records}
2302
+ font = by_code.get(DESIGN_FONT)
2303
+ if font is not None:
2304
+ own[font.id] = replace(font, value=theme.minor_font.encode("utf-16-le"))
2305
+ background = by_code.get(DESIGN_BACKGROUND)
2306
+ if background is not None:
2307
+ own[background.id] = replace(background, value=theme_color(theme, BACKGROUND_2))
2308
+ family = next((r for r in objects[0].records if r.code == DESIGN_FONT_FAMILY_SLOT[1]), None)
2309
+ _themed_face(own, theme.minor_font, family, DESIGN_FONT_FAMILY_SLOT)
2310
+ design = replace(objects[0], records=tuple(own[record_id] for record_id in sorted(own)))
2311
+ rest = tuple(replace(o, records=rethemed(o.records, theme, None)) for o in objects[1:])
2312
+ return build_design(header, (design, *rest), trailer)
2194
2313
 
2195
2314
 
2196
2315
  def with_prototype(
2197
- objects: tuple[DesignObject, ...], kind: str, type_code: int
2316
+ objects: tuple[DesignObject, ...], kind: str, type_code: int, theme: Theme = OFFICE_2007
2198
2317
  ) -> tuple[DesignObject, ...]:
2199
2318
  """The design with the control-defaults object for `type_code` in
2200
2319
  place: inserted in type order among the ones there, the run of
@@ -2203,7 +2322,7 @@ def with_prototype(
2203
2322
  ahead = list(objects[1:first_section])
2204
2323
  if type_code in DEFAULTS_FREE or any(o.type == type_code for o in ahead):
2205
2324
  return objects
2206
- fresh = DesignObject(None, type_code, None, prototype_records(kind, type_code))
2325
+ fresh = DesignObject(None, type_code, None, prototype_records(kind, type_code, theme))
2207
2326
  at = next((i for i, o in enumerate(ahead) if (o.type or 0) > type_code), len(ahead))
2208
2327
  ahead.insert(at, fresh)
2209
2328
  sections = [(i, o) for i, o in enumerate(objects) if o.is_section]
@@ -2296,6 +2415,8 @@ BUTTON_FILL_COMPANIONS: tuple[tuple[str, object], ...] = (("Gradient", 0),)
2296
2415
  #: from the installed font; these are the values it wrote for the fonts
2297
2416
  #: below (measured on 24 fonts), and a font not listed gets its name alone.
2298
2417
  FONT_FAMILIES: dict[str, int] = {
2418
+ "Aptos": 0,
2419
+ "Aptos Display": 0,
2299
2420
  "Book Antiqua": 18,
2300
2421
  "Cambria": 18,
2301
2422
  "Comic Sans MS": 66,
@@ -2467,6 +2588,7 @@ def add_control(
2467
2588
  height: int = 240,
2468
2589
  caption: str | None = None,
2469
2590
  kind: str = "form",
2591
+ theme: Theme = OFFICE_2007,
2470
2592
  ) -> bytes:
2471
2593
  """A design with one more control on it.
2472
2594
 
@@ -2480,7 +2602,8 @@ def add_control(
2480
2602
  the section's.
2481
2603
 
2482
2604
  The first control of a type also brings the type's control-defaults
2483
- object ahead of the sections, as it does in Access (`with_prototype`).
2605
+ object ahead of the sections, as it does in Access (`with_prototype`),
2606
+ in the fonts and colours of `theme`, the database's.
2484
2607
  """
2485
2608
  header, objects, trailer = parse_design(blob)
2486
2609
  if any(o.name == name for o in objects):
@@ -2512,7 +2635,7 @@ def add_control(
2512
2635
  )
2513
2636
  # The first control of a type brings the type's control-defaults object
2514
2637
  # ahead of the sections, which moves the sections along.
2515
- objects = with_prototype(objects, kind, TYPE_CODES[control_type])
2638
+ objects = with_prototype(objects, kind, TYPE_CODES[control_type], theme)
2516
2639
  at = next(i for i, o in enumerate(objects) if o.is_section and o.name == section)
2517
2640
  end = at + 1
2518
2641
  while end < len(objects) and not objects[end].is_section:
@@ -70,11 +70,9 @@ ACTION_IDS: dict[str, int] = {
70
70
  }
71
71
  ACTION_NAMES: dict[int, str] = {value: key for key, value in ACTION_IDS.items()}
72
72
 
73
- #: `MSysObjects.Type` and `MSysNavPaneObjectIDs.Type` for a macro. A
74
- #: macro's object id steps by one where a module's steps by four.
73
+ #: `MSysObjects.Type` and `MSysNavPaneObjectIDs.Type` for a macro.
75
74
  OBJECT_MACRO = -32766
76
75
  NAV_MACRO_TYPE = 32770
77
- OBJECT_ID_STEP = 1
78
76
 
79
77
 
80
78
  @dataclass(frozen=True)