athena-python-docx 0.18.3__tar.gz → 0.20.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 (365) hide show
  1. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/CLAUDE.md +74 -0
  2. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/PKG-INFO +1 -1
  3. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/__init__.py +1 -1
  4. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_buffer.py +40 -6
  5. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_http_doc.py +88 -17
  6. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_image_utils.py +160 -1
  7. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/comments.py +6 -2
  8. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/document.py +73 -16
  9. athena_python_docx-0.20.0/docx/enum/shape.py +30 -0
  10. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/style.py +17 -68
  11. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/errors.py +34 -0
  12. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/opc/coreprops.py +45 -0
  13. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/section.py +75 -33
  14. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/shape.py +23 -6
  15. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/shared.py +10 -8
  16. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/styles/styles.py +45 -20
  17. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/table.py +43 -31
  18. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/hyperlink.py +13 -11
  19. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/paragraph.py +4 -1
  20. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/parfmt.py +25 -22
  21. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/run.py +50 -11
  22. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/pyproject.toml +1 -1
  23. athena_python_docx-0.20.0/scripts/dump_wire_fixtures.py +59 -0
  24. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/release.sh +9 -12
  25. athena_python_docx-0.20.0/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +3 -0
  26. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/intentional_deviations.json +0 -2
  27. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/reports/GAP_ANALYSIS.md +5 -33
  28. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/reports/gap_report.json +9 -1688
  29. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/snapshots/athena_latest.json +2812 -580
  30. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_batching_perf.py +4 -8
  31. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_buffer.py +119 -4
  32. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_commands.py +4 -3
  33. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_http_transport.py +159 -6
  34. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_parity_misc.py +35 -6
  35. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_ptc.py +2 -2
  36. athena_python_docx-0.20.0/tests/test_python_docx_api_parity.py +357 -0
  37. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_revisions.py +18 -47
  38. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_hyperlink.py +6 -4
  39. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_parfmt.py +37 -17
  40. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_section_dimensions.py +36 -17
  41. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/uv.lock +1 -1
  42. athena_python_docx-0.18.3/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -1
  43. athena_python_docx-0.18.3/tests/test_python_docx_api_parity.py +0 -161
  44. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/.gitignore +0 -0
  45. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/DOCX_EXEC_LAB.md +0 -0
  46. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/README.md +0 -0
  47. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_athena_extension.py +0 -0
  48. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_batching.py +0 -0
  49. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_execution.py +0 -0
  50. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_http.py +0 -0
  51. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_postproc.py +0 -0
  52. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_ptc.py +0 -0
  53. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_table_styles.py +0 -0
  54. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_timeouts.py +0 -0
  55. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/api.py +0 -0
  56. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/bookmarks.py +0 -0
  57. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/charts.py +0 -0
  58. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/client.py +0 -0
  59. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/commands.py +0 -0
  60. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/__init__.py +0 -0
  61. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/section.py +0 -0
  62. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/table.py +0 -0
  63. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/text.py +0 -0
  64. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/exceptions.py +0 -0
  65. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/fields.py +0 -0
  66. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/footnotes.py +0 -0
  67. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/math.py +0 -0
  68. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/opc/__init__.py +0 -0
  69. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/oxml/__init__.py +0 -0
  70. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/revisions.py +0 -0
  71. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/sdt.py +0 -0
  72. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/session.py +0 -0
  73. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/settings.py +0 -0
  74. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/styles/__init__.py +0 -0
  75. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/styles/style.py +0 -0
  76. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/__init__.py +0 -0
  77. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/font.py +0 -0
  78. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/pagebreak.py +0 -0
  79. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/tabstops.py +0 -0
  80. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/toc.py +0 -0
  81. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/typing.py +0 -0
  82. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/docx_exec_lab.py +0 -0
  83. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/docx_exec_lab_examples/fast_table_fill.py +0 -0
  84. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/docx_exec_lab_examples/find_replace_literal.py +0 -0
  85. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/docx_exec_lab_server.py +0 -0
  86. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/publish.sh +0 -0
  87. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/round_trip_smoke.py +0 -0
  88. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/smoke_test_block_not_found.py +0 -0
  89. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/validate_find_replace_asset.py +0 -0
  90. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/__init__.py +0 -0
  91. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/conftest.py +0 -0
  92. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/METHODOLOGY.md +0 -0
  93. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/README.md +0 -0
  94. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/__init__.py +0 -0
  95. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/ab_probe_cases.py +0 -0
  96. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/ab_probe_runner.py +0 -0
  97. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/auto_gen_cases.py +0 -0
  98. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/binary_round_trip.py +0 -0
  99. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/cases.py +0 -0
  100. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/complex_cases.py +0 -0
  101. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/coverage_report.py +0 -0
  102. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/extract.py +0 -0
  103. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/extreme_cases.py +0 -0
  104. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/fake_session.py +0 -0
  105. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/README.md +0 -0
  106. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/__init__.py +0 -0
  107. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/_runner.py +0 -0
  108. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/extractor.py +0 -0
  109. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/test_pw_corpus.py +0 -0
  110. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/test_pw_research_digest.py +0 -0
  111. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/local_runner.py +0 -0
  112. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/mega_cases.py +0 -0
  113. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshot.py +0 -0
  114. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
  115. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
  116. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
  117. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
  118. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
  119. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
  120. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
  121. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
  122. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
  123. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
  124. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
  125. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
  126. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
  127. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
  128. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
  129. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
  130. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
  131. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
  132. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
  133. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
  134. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
  135. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
  136. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
  137. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
  138. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
  139. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
  140. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
  141. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
  142. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
  143. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
  144. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
  145. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
  146. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
  147. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
  148. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
  149. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
  150. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
  151. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
  152. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
  153. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
  154. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
  155. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
  156. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
  157. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
  158. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
  159. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
  160. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
  161. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
  162. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
  163. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
  164. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
  165. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
  166. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
  167. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
  168. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
  169. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
  170. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
  171. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
  172. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
  173. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
  174. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
  175. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
  176. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
  177. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
  178. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
  179. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
  180. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
  181. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
  182. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
  183. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
  184. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
  185. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
  186. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
  187. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
  188. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
  189. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
  190. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
  191. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
  192. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
  193. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
  194. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
  195. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
  196. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
  197. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
  198. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
  199. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
  200. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
  201. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
  202. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
  203. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
  204. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
  205. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
  206. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
  207. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
  208. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
  209. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
  210. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
  211. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
  212. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
  213. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
  214. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
  215. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
  216. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
  217. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
  218. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
  219. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
  220. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
  221. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
  222. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
  223. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
  224. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
  225. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
  226. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
  227. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
  228. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
  229. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
  230. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
  231. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
  232. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
  233. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
  234. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
  235. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
  236. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
  237. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
  238. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
  239. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
  240. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
  241. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
  242. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
  243. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
  244. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
  245. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
  246. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
  247. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
  248. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
  249. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
  250. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
  251. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
  252. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
  253. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
  254. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
  255. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
  256. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
  257. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
  258. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
  259. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
  260. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/ours_spec.json +0 -0
  261. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/parity_crawl.py +0 -0
  262. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/parity_diff.json +0 -0
  263. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/real_world_cases.py +0 -0
  264. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/round_trip_tests.py +0 -0
  265. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/runner.py +0 -0
  266. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/stock_spec.json +0 -0
  267. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/test_e2e_against_staging.py +0 -0
  268. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/README.md +0 -0
  269. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/__init__.py +0 -0
  270. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/baseline_gaps.json +0 -0
  271. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/compare.py +0 -0
  272. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/introspect.py +0 -0
  273. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/run_parity.py +0 -0
  274. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
  275. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/test_parity_gap.py +0 -0
  276. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_add_section_extract_items.py +0 -0
  277. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_athena_extensions_contract.py +0 -0
  278. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_athena_extensions_registry.py +0 -0
  279. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_block_not_found_error.py +0 -0
  280. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_add_paragraph_wire_shape.py +0 -0
  281. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_add_table_not_supported.py +0 -0
  282. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_add_hyperlink_stash.py +0 -0
  283. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_add_run_via_cell_insert.py +0 -0
  284. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_format_stash.py +0 -0
  285. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_run_format_stash.py +0 -0
  286. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_run_guard.py +0 -0
  287. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_text_plain_fastpath.py +0 -0
  288. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_text_replace_semantics.py +0 -0
  289. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_collapsed_range_format.py +0 -0
  290. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_command_dataclasses.py +0 -0
  291. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_comments.py +0 -0
  292. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_asset_id_property.py +0 -0
  293. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_clear.py +0 -0
  294. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_create.py +0 -0
  295. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_create_from_template.py +0 -0
  296. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_factory_validation.py +0 -0
  297. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_docx_exec_lab.py +0 -0
  298. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_docx_exec_lab_server.py +0 -0
  299. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_e2e_partial_failure_cascade.py +0 -0
  300. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_execution_scope.py +0 -0
  301. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_find_replace_session_open.py +0 -0
  302. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_hyperlink_coalescing.py +0 -0
  303. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_image_url_data_uri.py +0 -0
  304. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_insert_deferred.py +0 -0
  305. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_iter_inner_content.py +0 -0
  306. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_list_styles.py +0 -0
  307. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_merged_cell_secondary_slot.py +0 -0
  308. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_merged_cells.py +0 -0
  309. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_oxml_shim.py +0 -0
  310. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_paragraph_text_len_cache.py +0 -0
  311. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_parity_round2.py +0 -0
  312. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_partial_failure_cascade.py +0 -0
  313. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_phase_a_behavior.py +0 -0
  314. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_phase_b_headers_footers.py +0 -0
  315. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_phase_c_tables.py +0 -0
  316. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_positional_cell_id.py +0 -0
  317. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_postproc_cell_format_rewrite.py +0 -0
  318. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_postproc_cell_run_format_rewrite.py +0 -0
  319. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_postproc_ref_restore.py +0 -0
  320. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_pr19766_review_fixes.py +0 -0
  321. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_add_paragraph_style.py +0 -0
  322. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_add_picture.py +0 -0
  323. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_add_run.py +0 -0
  324. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
  325. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_comments_add_comment.py +0 -0
  326. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_comments_get.py +0 -0
  327. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_document_audit.py +0 -0
  328. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_document_element.py +0 -0
  329. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_enum_section.py +0 -0
  330. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_font_audit.py +0 -0
  331. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_header_footer.py +0 -0
  332. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_inline_shape.py +0 -0
  333. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
  334. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_misc.py +0 -0
  335. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_paragraph_strict.py +0 -0
  336. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_paragraph_style.py +0 -0
  337. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_paragraph_style_strict.py +0 -0
  338. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_row_col_cell.py +0 -0
  339. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_add_break.py +0 -0
  340. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_bool_setters.py +0 -0
  341. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_style.py +0 -0
  342. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_style_strict.py +0 -0
  343. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_text.py +0 -0
  344. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_underline.py +0 -0
  345. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_section_audit.py +0 -0
  346. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_section_onoff.py +0 -0
  347. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_settings.py +0 -0
  348. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_shared_audit.py +0 -0
  349. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_style.py +0 -0
  350. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_styles.py +0 -0
  351. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_table_audit.py +0 -0
  352. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_table_cell.py +0 -0
  353. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_table_dimensions.py +0 -0
  354. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_table_layout.py +0 -0
  355. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_smoke_integration.py +0 -0
  356. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_style_acceptance.py +0 -0
  357. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_style_font.py +0 -0
  358. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_style_setters_contract.py +0 -0
  359. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_table_set_cell_perf.py +0 -0
  360. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_table_style_id_resolution.py +0 -0
  361. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_temporarily_unavailable.py +0 -0
  362. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_validate_find_replace_asset_script.py +0 -0
  363. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_wire_contract.py +0 -0
  364. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_xml_attr_guard.py +0 -0
  365. {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_zod_wire_contract.py +0 -0
@@ -601,6 +601,80 @@ natively, the SDK should swap to the upstream signature in place;
601
601
  this section is the canonical reference for what each Athena
602
602
  extension wires up.
603
603
 
604
+ ### Behavioral-parity deviations (2026 deep-dive, v0.19.0)
605
+
606
+ A deep behavioral audit against python-docx 1.2.0 (the structural
607
+ parity ratchet only sees shape, not runtime behavior) fixed ~40
608
+ divergences so they now match upstream exactly — `Pt`/`RGBColor`
609
+ coercion, `Font.size` bare-int-is-EMU, `Font.underline`
610
+ SINGLE→`True`/NONE→`False`, `line_spacing` integer multipliers,
611
+ `line_spacing_rule` rule-only members, `Hyperlink.url`/`.address`,
612
+ `Table.autofit`/`_Row.cells`/`_Column.cells`, `Section`
613
+ orientation/start-type defaults + `Sections` slicing,
614
+ `Styles.get_by_id`/`get_style_id`/`default(LIST)`/`add_style`,
615
+ `Document.add_heading` (`ValueError`), `add_paragraph` strict heading
616
+ routing, `add_picture` native size + aspect ratio, `WD_BUILTIN_STYLE`
617
+ (now the 132-member int enum aliased to `WD_STYLE`),
618
+ `docx.enum.shape.WD_INLINE_SHAPE`, `InlineShape.type`/zero-dimensions,
619
+ and `CoreProperties` validation. These are not deviations — they close
620
+ gaps. The items below are the **residual intentional deviations** that
621
+ remain because matching upstream would break the asset-backed/HTTP
622
+ architecture or a SuperDoc constraint:
623
+
624
+ - **`WD_*` enum `.value` is the SuperDoc wire string, not the MS Word
625
+ integer constant.** `WD_ALIGN_PARAGRAPH.CENTER.value == "center"`
626
+ (upstream: `1`); `int(member)` / `member == 1` / `WD_*(1)` therefore
627
+ differ. The string value is load-bearing — every command serializes
628
+ `enum.value` directly onto the wire (`underline=value.value`,
629
+ `WD_COLOR_INDEX(v).value`, …), so int-valuing the enums would require
630
+ re-plumbing the entire command layer. Members are accessed by name
631
+ (`WD_ALIGN_PARAGRAPH.CENTER`), so the drift is academic for normal
632
+ use. (`WD_STYLE`/`WD_BUILTIN_STYLE` and `WD_INLINE_SHAPE_TYPE` are NOT
633
+ wire-load-bearing and DO carry the upstream int values.)
634
+
635
+ - **`RGBColor.from_string` additionally accepts a leading `#`.** Upstream
636
+ rejects `"#FF0000"`; agents routinely pass it, so a single `#` is
637
+ stripped before the (otherwise byte-identical) upstream slice parse.
638
+ A strict superset — every value upstream accepts parses identically.
639
+
640
+ - **`_Cell.vertical_alignment` setter accepts the four SuperDoc wire
641
+ tokens** (`"top"`/`"center"`/`"bottom"`/`"both"`) in addition to
642
+ `WD_ALIGN_VERTICAL` members. Upstream rejects all plain strings; this
643
+ is an ergonomic superset (typos like `"middle"` still raise
644
+ `ValueError`).
645
+
646
+ - **`Table.cell(row, col)` bounds-checks each axis independently** and
647
+ raises `IndexError` for an out-of-range row or column. Upstream
648
+ indexes a single flat row-major `_cells` list, so a column overflow
649
+ spills into the next row and `cell(-1, -1)` resolves against the flat
650
+ list (NOT the bottom-right cell). Athena's per-axis contract is the
651
+ more intuitive, stricter behavior for an HTTP-backed proxy that can't
652
+ cheaply enumerate every cell. `(0,0)` and in-range negative indices
653
+ behave identically to upstream.
654
+
655
+ - **`BaseStyle.style_id` returns the display name for built-ins** (so
656
+ `style_id == name`). The SDK is HTTP-only and can't read `styles.xml`
657
+ to recover the OOXML styleId (`"Heading 1"` → `"Heading1"`); the wire
658
+ format also uses the display name as the style id. Consequently
659
+ `get_by_id("Heading1", …)` / `"Heading1" in styles` won't resolve the
660
+ OOXML-id form (use the display name).
661
+
662
+ - **`Document.add_table(rows, cols)` requires `rows >= 1` and
663
+ `cols >= 1`.** Upstream allows a 0-row/0-col grid you later populate,
664
+ but SuperDoc's ProseMirror table node can't represent one; the SDK
665
+ raises eagerly rather than failing opaquely on the wire.
666
+
667
+ - **`Comment.comment_id` returns `str`, not `int`.** SuperDoc uses
668
+ non-numeric entity ids (`"c-42"`), so the upstream `int` contract
669
+ can't be honored. Tracked in `SUPERDOC_UPSTREAM_REQUESTS.md`.
670
+
671
+ - **`Section` dimension setters and `ParagraphFormat.line_spacing_rule
672
+ = None` warn-and-preserve on a clear** rather than raising (upstream
673
+ accepts `None` to clear, but SuperDoc's merging mutation ops have no
674
+ single-attribute clear). They emit a `Pending…ClearWarning` and leave
675
+ the value intact instead of destructively clearing the whole block.
676
+ Tracked in `SUPERDOC_UPSTREAM_REQUESTS.md`.
677
+
604
678
  ### If you need a deviation
605
679
 
606
680
  If there is a genuine technical reason why a deviation from python-docx is necessary:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-docx
3
- Version: 0.18.3
3
+ Version: 0.20.0
4
4
  Summary: Drop-in replacement for python-docx that connects to Athena's Superdoc/Keryx collaborative document stack
5
5
  Project-URL: Homepage, https://athenaintelligence.ai
6
6
  Author-email: Athena Intelligence <engineering@athenaintelligence.ai>
@@ -6,7 +6,7 @@ See CLAUDE.md for the API parity contract.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- __version__ = "0.18.3"
9
+ __version__ = "0.20.0"
10
10
 
11
11
  from docx.api import Document
12
12
  from docx._buffer import flush_all
@@ -41,7 +41,7 @@ from docx._execution import (
41
41
  )
42
42
  from docx._timeouts import timeout_for_commands
43
43
  from docx.commands import Command, is_response_bearing, must_flush_immediately
44
- from docx.errors import DocxError, FlushAllError
44
+ from docx.errors import DocxError, DocxUnsupportedCommandError, FlushAllError
45
45
 
46
46
 
47
47
  def _apply_proxy_id_rewrites(
@@ -467,8 +467,17 @@ class CommandBuffer:
467
467
  def flush(self) -> list[Any]:
468
468
  """Flush pending commands as one HTTP batch.
469
469
 
470
- Returns the list of per-command result dicts. Empty list if nothing
471
- was pending. Cancels any active idle timer.
470
+ Returns the per-command result list, positionally aligned with the
471
+ flushed commands (skipped commands occupy their slot as ``None``).
472
+ Empty list if nothing was pending. Cancels any active idle timer.
473
+
474
+ This is the FIRE-AND-FORGET path: callers here don't await an
475
+ individual command's result, so an unsupported command that the
476
+ server skipped is simply warned (inside ``execute_batch``) and its
477
+ slot left ``None`` — the supported commands still applied. (A caller
478
+ that DOES need a single command's result goes through
479
+ :meth:`_eager_flush_with`, which raises on a skipped trailing command
480
+ rather than returning the wrong slot.)
472
481
 
473
482
  After the batch returns, walks per-cmd results for
474
483
  ``real_node_id`` / ``real_entity_id`` echoes and updates any
@@ -490,7 +499,7 @@ class CommandBuffer:
490
499
  if not pending:
491
500
  return []
492
501
  try:
493
- results = self._client.execute_batch(
502
+ results, _skipped_by_index = self._client.execute_batch(
494
503
  self._asset_id,
495
504
  pending,
496
505
  change_mode=change_mode,
@@ -554,8 +563,11 @@ class CommandBuffer:
554
563
  proxy_id_refs = self._proxy_id_refs
555
564
  self._proxy_id_refs = {}
556
565
  all_cmds: list[Command] = [*pending, cmd]
566
+ trailing_index = len(all_cmds) - 1
557
567
  try:
558
- results: list[Any] = self._client.execute_batch(
568
+ results: list[Any]
569
+ skipped_by_index: dict[int, dict[str, Any]]
570
+ results, skipped_by_index = self._client.execute_batch(
559
571
  self._asset_id,
560
572
  all_cmds,
561
573
  change_mode=change_mode,
@@ -576,10 +588,32 @@ class CommandBuffer:
576
588
  _ptc_emit_end_batch(all_cmds, is_error=True)
577
589
  raise
578
590
  _ptc_emit_end_batch(all_cmds, is_error=False)
591
+ # Proxy-id rewrites still run for the buffered prefix that DID apply —
592
+ # the trailing skip (if any) doesn't strand the supported Creates that
593
+ # flushed alongside it.
579
594
  _apply_proxy_id_rewrites(results, proxy_id_refs)
595
+ # This is the AWAITED-result path: the caller is about to read the
596
+ # trailing command's result. If the server skipped that command
597
+ # (capability gap), there is no result to return — returning some
598
+ # earlier applied mutation's slot would be silent WRONG DATA. Fail
599
+ # loud instead. (Fire-and-forget mutations never reach here; they go
600
+ # through :meth:`flush`, which keeps the skip+warn behavior.)
601
+ if trailing_index in skipped_by_index:
602
+ skip = skipped_by_index[trailing_index]
603
+ reason = skip.get("reason") if isinstance(skip, dict) else None
604
+ raise DocxUnsupportedCommandError(
605
+ f"docx-studio skipped command {type(cmd).__name__!r} but the "
606
+ f"SDK was awaiting its result" + (f": {reason}" if reason else ".")
607
+ )
580
608
  if not results:
581
609
  return {}
582
- return results[-1]
610
+ # Trailing command applied: its slot holds the result dict (or ``{}``
611
+ # for a non-dict result). A ``None`` here would only arise if the
612
+ # server omitted the trailing command's index from ``applied[]``
613
+ # without listing it in ``skipped[]`` — fall back to ``{}`` to match
614
+ # the legacy "no result" behavior rather than handing back ``None``.
615
+ trailing = results[-1]
616
+ return trailing if trailing is not None else {}
583
617
 
584
618
  def _reset_timer_locked(self) -> None:
585
619
  # Caller must hold self._lock.
@@ -31,6 +31,7 @@ from __future__ import annotations
31
31
 
32
32
  import json
33
33
  import os
34
+ import warnings
34
35
  from typing import Any
35
36
 
36
37
  import requests
@@ -172,6 +173,7 @@ from docx.errors import (
172
173
  BlockNotFoundError,
173
174
  DocxError,
174
175
  DocxStudioTemporarilyUnavailable,
176
+ DocxUnsupportedCommandWarning,
175
177
  NotFoundError,
176
178
  SessionError,
177
179
  )
@@ -662,6 +664,29 @@ def _http_post_json(
662
664
  )
663
665
 
664
666
 
667
+ def _warn_skipped_commands(skipped: list[Any]) -> None:
668
+ """Emit one :class:`DocxUnsupportedCommandWarning` per server-reported
669
+ skipped command.
670
+
671
+ Each entry is the wire-shape ``{index, type, reason}`` dict the applier
672
+ records when a command raises ``UnsupportedDocxEditorCommandError``. The
673
+ batch still succeeded for every supported command; warning (rather than
674
+ raising) is the graceful-degradation contract so a script that ends with
675
+ an unsupported op doesn't abort into an empty document.
676
+
677
+ Tolerant of malformed entries: anything that isn't a dict is summarized
678
+ generically rather than crashing the (otherwise successful) batch.
679
+ """
680
+ for entry in skipped:
681
+ if isinstance(entry, dict):
682
+ cmd_type = entry.get("type", "<unknown>")
683
+ reason = entry.get("reason") or "not supported by the docx-editor engine yet"
684
+ message = f"Skipped unsupported command {cmd_type!r}: {reason}"
685
+ else:
686
+ message = f"Skipped unsupported command: {entry!r}"
687
+ warnings.warn(message, DocxUnsupportedCommandWarning, stacklevel=2)
688
+
689
+
665
690
  # ---------------------------------------------------------------------------
666
691
  # HttpClient — the bare HTTP transport. Stateless wrt batching.
667
692
  # ---------------------------------------------------------------------------
@@ -706,8 +731,22 @@ class HttpClient:
706
731
  *,
707
732
  change_mode: str | None = None,
708
733
  user: dict[str, str] | None = None,
709
- ) -> list[Any]:
710
- """POST a batch of typed commands, return per-command result dicts.
734
+ ) -> tuple[list[Any], dict[int, dict[str, Any]]]:
735
+ """POST a batch of typed commands.
736
+
737
+ Returns ``(results, skipped_by_index)``:
738
+
739
+ - ``results`` is **positionally aligned** with ``commands``: one slot
740
+ per input command, in order. An applied command's slot holds its
741
+ ``result`` dict (or ``{}`` for a non-dict result); a SKIPPED
742
+ command's slot holds ``None``. Positional alignment is what lets a
743
+ caller awaiting a single command's result (``_eager_flush_with``)
744
+ map ``results[-1]`` back to *its* command and detect that the
745
+ command was skipped (``None``) rather than silently returning some
746
+ earlier applied mutation's result.
747
+ - ``skipped_by_index`` maps the input index of each skipped command to
748
+ its ``{index, type, reason}`` entry, so callers awaiting a result a
749
+ skipped command can't provide can raise a precise error.
711
750
 
712
751
  Envelope fields:
713
752
  change_mode: ``"direct"`` | ``"tracked"`` | ``None``. When
@@ -720,14 +759,22 @@ class HttpClient:
720
759
  opens the per-asset session for the first time; ignored
721
760
  on subsequent batches against the same pooled session.
722
761
 
723
- On success, length matches len(commands). On 207 partial-failure,
724
- the server returns 207 with `applied` (the successful prefix) plus
725
- an `error` for the failing command :func:`_http_post_json`
726
- translates that to a :class:`DocxError`. We don't pad partial
727
- results because the doc state is undefined.
762
+ On success, ``len(applied) + len(skipped) == len(commands)``. The
763
+ server's ``skipped[]`` array carries commands the docx-editor engine
764
+ doesn't implement yet; each is surfaced as a
765
+ :class:`DocxUnsupportedCommandWarning` (graceful degradation one
766
+ unsupported FIRE-AND-FORGET mutation no longer aborts the whole batch
767
+ into an empty document). Fire-and-forget mutations don't read their
768
+ result, so a warning + skip is correct for them. A skipped command
769
+ that a caller IS awaiting (a query / response-bearing op routed
770
+ through ``_eager_flush_with``) is detected positionally and raised by
771
+ that caller. On 207 partial-failure, the server returns 207 with
772
+ `applied` (the successful prefix) plus an `error` for the failing
773
+ command — :func:`_http_post_json` translates that to a
774
+ :class:`DocxError`.
728
775
  """
729
776
  if not commands:
730
- return []
777
+ return [], {}
731
778
 
732
779
  url: str = f"{self._base_url}/docs/{asset_id}/commands"
733
780
  body: dict[str, Any] = {
@@ -747,6 +794,12 @@ class HttpClient:
747
794
  timeout=timeout_for_commands(commands=commands),
748
795
  )
749
796
  applied = resp.get("applied")
797
+ # Commands the engine doesn't implement yet are reported in
798
+ # ``skipped[]`` (non-fatal). Warn per skip and count them toward the
799
+ # batch total — they're a success, not a failure.
800
+ skipped_raw = resp.get("skipped")
801
+ skipped = skipped_raw if isinstance(skipped_raw, list) else []
802
+ _warn_skipped_commands(skipped)
750
803
  # An empty applied list paired with an `error` field is the
751
804
  # 'all-commands-failed' shape — surface it rather than returning
752
805
  # an empty result list to a caller that's about to index [-1].
@@ -758,19 +811,37 @@ class HttpClient:
758
811
  f"docx-studio response missing applied[]: {resp!r}",
759
812
  )
760
813
 
761
- if len(applied) != len(commands):
814
+ # Applied + skipped together must account for every command. A short
815
+ # ``applied`` is only legitimate when the remainder were skipped.
816
+ if len(applied) + len(skipped) != len(commands):
762
817
  raise DocxError(
763
- f"docx-studio applied {len(applied)} of {len(commands)} commands",
818
+ f"docx-studio applied {len(applied)} (+{len(skipped)} skipped) "
819
+ f"of {len(commands)} commands",
764
820
  )
765
821
 
766
- results: list[Any] = []
822
+ # Build a positionally-aligned result list (one slot per input
823
+ # command). Applied slots carry the result dict; skipped slots stay
824
+ # ``None``. Each entry's ``index`` field places it at the right slot —
825
+ # we don't assume ``applied[]`` is a contiguous prefix once skips can
826
+ # appear anywhere in the batch.
827
+ results: list[Any] = [None] * len(commands)
767
828
  for entry in applied:
768
- if isinstance(entry, dict):
769
- result = entry.get("result")
770
- results.append(result if isinstance(result, dict) else {})
771
- else:
772
- results.append({})
773
- return results
829
+ if not isinstance(entry, dict):
830
+ continue
831
+ idx = entry.get("index")
832
+ if not isinstance(idx, int) or not (0 <= idx < len(results)):
833
+ continue
834
+ result = entry.get("result")
835
+ results[idx] = result if isinstance(result, dict) else {}
836
+
837
+ skipped_by_index: dict[int, dict[str, Any]] = {}
838
+ for entry in skipped:
839
+ if not isinstance(entry, dict):
840
+ continue
841
+ idx = entry.get("index")
842
+ if isinstance(idx, int) and 0 <= idx < len(commands):
843
+ skipped_by_index[idx] = entry
844
+ return results, skipped_by_index
774
845
 
775
846
 
776
847
  # ---------------------------------------------------------------------------
@@ -211,4 +211,163 @@ def _decode_image_data_uri(uri: str) -> bytes:
211
211
  raise ValueError(f"add_picture could not decode data: URI: {exc}") from exc
212
212
 
213
213
 
214
- __all__ = ["load_image_bytes", "sniff_content_type"]
214
+ # ---------------------------------------------------------------------------
215
+ # Native dimension / DPI sniffing — parity with python-docx's add_picture
216
+ # native-size + aspect-ratio behavior (no Pillow dependency).
217
+ # ---------------------------------------------------------------------------
218
+
219
+ _EMU_PER_INCH: int = 914400
220
+
221
+
222
+ def _png_dimensions(b: bytes) -> "tuple[int, int, float, float] | None":
223
+ # IHDR (width/height) sits right after the 8-byte signature in the
224
+ # first chunk: [4-byte len][b"IHDR"][4-byte width][4-byte height]...
225
+ if len(b) < 24 or b[12:16] != b"IHDR":
226
+ return None
227
+ px_w = int.from_bytes(b[16:20], "big")
228
+ px_h = int.from_bytes(b[20:24], "big")
229
+ if px_w <= 0 or px_h <= 0:
230
+ return None
231
+ horz_dpi = vert_dpi = 72.0 # python-docx's PNG default
232
+ # Optional pHYs chunk carries pixels-per-unit; unit 1 == metre.
233
+ idx = b.find(b"pHYs")
234
+ if idx != -1 and idx + 13 <= len(b):
235
+ ppu_x = int.from_bytes(b[idx + 4 : idx + 8], "big")
236
+ ppu_y = int.from_bytes(b[idx + 8 : idx + 12], "big")
237
+ unit = b[idx + 12]
238
+ if unit == 1 and ppu_x > 0 and ppu_y > 0:
239
+ # python-docx rounds DPI to an int (``int(round(ppu*0.0254))``),
240
+ # so match that to produce identical native EMU dimensions.
241
+ horz_dpi = float(round(ppu_x * 0.0254))
242
+ vert_dpi = float(round(ppu_y * 0.0254))
243
+ return px_w, px_h, horz_dpi, vert_dpi
244
+
245
+
246
+ def _gif_dimensions(b: bytes) -> "tuple[int, int, float, float] | None":
247
+ if len(b) < 10:
248
+ return None
249
+ px_w = int.from_bytes(b[6:8], "little")
250
+ px_h = int.from_bytes(b[8:10], "little")
251
+ if px_w <= 0 or px_h <= 0:
252
+ return None
253
+ # GIF carries no resolution; python-docx defaults to 72 DPI.
254
+ return px_w, px_h, 72.0, 72.0
255
+
256
+
257
+ def _bmp_dimensions(b: bytes) -> "tuple[int, int, float, float] | None":
258
+ if len(b) < 26:
259
+ return None
260
+ px_w = int.from_bytes(b[18:22], "little", signed=True)
261
+ px_h = abs(int.from_bytes(b[22:26], "little", signed=True))
262
+ if px_w <= 0 or px_h <= 0:
263
+ return None
264
+ horz_dpi = vert_dpi = 96.0 # python-docx's BMP default
265
+ if len(b) >= 46:
266
+ ppm_x = int.from_bytes(b[38:42], "little", signed=True)
267
+ ppm_y = int.from_bytes(b[42:46], "little", signed=True)
268
+ if ppm_x > 0 and ppm_y > 0:
269
+ horz_dpi = float(round(ppm_x * 0.0254))
270
+ vert_dpi = float(round(ppm_y * 0.0254))
271
+ return px_w, px_h, horz_dpi, vert_dpi
272
+
273
+
274
+ def _jpeg_dimensions(b: bytes) -> "tuple[int, int, float, float] | None":
275
+ # Default density per python-docx's JFIF handling.
276
+ horz_dpi = vert_dpi = 72.0
277
+ px_w = px_h = 0
278
+ n = len(b)
279
+ pos = 2 # skip SOI (FF D8)
280
+ while pos + 4 <= n:
281
+ if b[pos] != 0xFF:
282
+ pos += 1
283
+ continue
284
+ marker = b[pos + 1]
285
+ if marker in (0xD8, 0xD9) or 0xD0 <= marker <= 0xD7:
286
+ pos += 2
287
+ continue
288
+ seg_len = int.from_bytes(b[pos + 2 : pos + 4], "big")
289
+ if seg_len < 2:
290
+ break
291
+ seg = b[pos + 4 : pos + 2 + seg_len]
292
+ if marker == 0xE0 and seg[:5] == b"JFIF\x00" and len(seg) >= 12:
293
+ units = seg[7]
294
+ dx = int.from_bytes(seg[8:10], "big")
295
+ dy = int.from_bytes(seg[10:12], "big")
296
+ if units == 1 and dx and dy: # dots per inch
297
+ horz_dpi, vert_dpi = float(dx), float(dy)
298
+ elif units == 2 and dx and dy: # dots per cm
299
+ horz_dpi, vert_dpi = dx * 2.54, dy * 2.54
300
+ # SOFn frame markers carry the actual dimensions.
301
+ elif marker in (
302
+ 0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7,
303
+ 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF,
304
+ ) and len(seg) >= 5:
305
+ px_h = int.from_bytes(seg[1:3], "big")
306
+ px_w = int.from_bytes(seg[3:5], "big")
307
+ break
308
+ pos += 2 + seg_len
309
+ if px_w <= 0 or px_h <= 0:
310
+ return None
311
+ return px_w, px_h, horz_dpi, vert_dpi
312
+
313
+
314
+ def sniff_image_dimensions(image_bytes: bytes) -> "tuple[int, int, float, float] | None":
315
+ """Return ``(px_width, px_height, horz_dpi, vert_dpi)`` for raster
316
+ image bytes, or ``None`` if the format/dimensions can't be parsed.
317
+
318
+ Mirrors the header parsers in python-docx's ``docx.image`` (PNG,
319
+ JPEG, GIF, BMP) without requiring Pillow. DPI defaults match
320
+ python-docx (72 for PNG/JPEG/GIF, 96 for BMP) when the image carries
321
+ no embedded resolution.
322
+ """
323
+ if image_bytes.startswith(_PNG_MAGIC):
324
+ return _png_dimensions(image_bytes)
325
+ if image_bytes.startswith(_JPEG_MAGIC):
326
+ return _jpeg_dimensions(image_bytes)
327
+ if image_bytes.startswith(_GIF87_MAGIC) or image_bytes.startswith(_GIF89_MAGIC):
328
+ return _gif_dimensions(image_bytes)
329
+ if image_bytes.startswith(_BMP_MAGIC):
330
+ return _bmp_dimensions(image_bytes)
331
+ return None
332
+
333
+
334
+ def scaled_dimensions_emu(
335
+ image_bytes: bytes,
336
+ width_emu: "int | None",
337
+ height_emu: "int | None",
338
+ ) -> "tuple[int, int] | None":
339
+ """Resolve the render size (in EMU) for ``add_picture``, mirroring
340
+ python-docx's ``Image.scaled_dimensions``.
341
+
342
+ - both width and height given -> use them verbatim.
343
+ - neither given -> the image's native size (from px + DPI).
344
+ - exactly one given -> compute the other to preserve aspect ratio.
345
+
346
+ Returns ``None`` only when a dimension must be derived but native
347
+ dimensions can't be sniffed — the caller then falls back to its
348
+ legacy fixed default.
349
+ """
350
+ if width_emu is not None and height_emu is not None:
351
+ return width_emu, height_emu
352
+ dims = sniff_image_dimensions(image_bytes)
353
+ if dims is None:
354
+ return None
355
+ px_w, px_h, dpi_x, dpi_y = dims
356
+ nat_w = int(round(px_w / dpi_x * _EMU_PER_INCH))
357
+ nat_h = int(round(px_h / dpi_y * _EMU_PER_INCH))
358
+ if width_emu is None and height_emu is None:
359
+ return nat_w, nat_h
360
+ if height_emu is None: # width supplied, scale height
361
+ scale = (float(width_emu) / float(nat_w)) if nat_w else 1.0
362
+ return width_emu, int(round(nat_h * scale))
363
+ # height supplied, scale width
364
+ scale = (float(height_emu) / float(nat_h)) if nat_h else 1.0
365
+ return int(round(nat_w * scale)), height_emu
366
+
367
+
368
+ __all__ = [
369
+ "load_image_bytes",
370
+ "sniff_content_type",
371
+ "sniff_image_dimensions",
372
+ "scaled_dimensions_emu",
373
+ ]
@@ -217,8 +217,12 @@ class Comment:
217
217
  self._info["creatorName"] = value
218
218
 
219
219
  @property
220
- def initials(self) -> str:
221
- return str(self._info.get("initials") or "")
220
+ def initials(self) -> str | None:
221
+ # python-docx's CT_Comment.initials is OptionalAttribute(str), so
222
+ # an absent/None initials reads back as None (not ""), letting
223
+ # callers distinguish "no initials set" from "empty initials".
224
+ val = self._info.get("initials")
225
+ return None if val is None else str(val)
222
226
 
223
227
  @initials.setter
224
228
  def initials(self, value: str) -> None:
@@ -1525,14 +1525,18 @@ class Document:
1525
1525
  #
1526
1526
  # For heading styles we still dispatch to add_heading, which uses
1527
1527
  # `doc.create.heading` (also canonical, takes `level`).
1528
- if style_str and style_str.lower().startswith("heading"):
1529
- try:
1530
- level: int = int(style_str.rsplit(" ", 1)[-1])
1531
- except ValueError:
1532
- level = 1
1533
- return self.add_heading(text=text, level=level)
1534
- if style_str and style_str.lower() == "title":
1535
- return self.add_heading(text=text, level=0)
1528
+ if style_str is not None:
1529
+ low: str = style_str.lower()
1530
+ # Only the canonical built-in heading ids (``Heading 1`` ..
1531
+ # ``Heading 9``) route to add_heading. A custom style whose
1532
+ # name merely starts with "heading" (e.g. "Heading Box",
1533
+ # "Heading Note") must be applied verbatim — the previous
1534
+ # loose ``startswith("heading")`` + parse-or-default-to-1
1535
+ # silently relabeled every such style as Heading 1.
1536
+ if low in {f"heading {n}" for n in range(1, 10)}:
1537
+ return self.add_heading(text=text, level=int(low.rsplit(" ", 1)[-1]))
1538
+ if low == "title":
1539
+ return self.add_heading(text=text, level=0)
1536
1540
 
1537
1541
  # python-docx parity: tabs and line breaks in `text` must
1538
1542
  # round-trip as Word ``<w:tab/>`` / ``<w:br/>`` elements, not
@@ -1725,9 +1729,9 @@ class Document:
1725
1729
  self._ensure_open()
1726
1730
  self._reset_list_chain()
1727
1731
  if not 0 <= level <= 9:
1728
- raise ValidationError(
1729
- f"level must be in 0..9; got {level}",
1730
- )
1732
+ # python-docx raises a builtin ``ValueError`` here (not an
1733
+ # SDK ValidationError) with this exact message.
1734
+ raise ValueError("level must be in range 0-9, got %d" % level)
1731
1735
 
1732
1736
  # python-docx parity: tabs/newlines in `text` must round-trip
1733
1737
  # as Word inlines, not literal characters. SuperDoc's
@@ -1797,9 +1801,18 @@ class Document:
1797
1801
 
1798
1802
  self._ensure_open()
1799
1803
  self._reset_list_chain()
1804
+ # Intentional deviation from python-docx (documented in CLAUDE.md):
1805
+ # upstream allows ``add_table(0, cols)`` to create an empty grid
1806
+ # you then populate with ``add_row()``, but SuperDoc's ProseMirror
1807
+ # table node requires at least one row and one column — a 0-row
1808
+ # table can't be represented — so we reject eagerly with a clear
1809
+ # error rather than failing opaquely on the wire.
1800
1810
  if rows < 1 or cols < 1:
1801
1811
  raise ValidationError(
1802
- f"rows and cols must be >= 1; got rows={rows} cols={cols}",
1812
+ f"rows and cols must be >= 1; got rows={rows} cols={cols}. "
1813
+ f"SuperDoc cannot represent a 0-row/0-col table — create "
1814
+ f"with the final dimensions (or 1x1, then add_row/"
1815
+ f"add_column).",
1803
1816
  )
1804
1817
 
1805
1818
  # The real param is `columns` (not `cols`). `at` is required to
@@ -1901,7 +1914,11 @@ class Document:
1901
1914
  server-side), or a ``data:`` URI. The URL and ``data:`` forms are
1902
1915
  Athena extensions beyond python-docx, which is path/stream-only.
1903
1916
  """
1904
- from docx._image_utils import load_image_bytes, sniff_content_type
1917
+ from docx._image_utils import (
1918
+ load_image_bytes,
1919
+ scaled_dimensions_emu,
1920
+ sniff_content_type,
1921
+ )
1905
1922
  from docx.shape import InlineShape
1906
1923
  from docx.text.run import _build_inline_shape_info
1907
1924
 
@@ -1931,8 +1948,32 @@ class Document:
1931
1948
  b64: str = base64.b64encode(image_bytes).decode("ascii")
1932
1949
  data_uri: str = f"data:{content_type};base64,{b64}"
1933
1950
 
1934
- w_px: float = _emu_to_px(width) if width is not None else 576.0
1935
- h_px: float = _emu_to_px(height) if height is not None else 432.0
1951
+ # python-docx parity: with no width/height the picture appears at
1952
+ # its native size (px / DPI); with exactly one dimension the other
1953
+ # is computed to preserve the aspect ratio. Only when the native
1954
+ # dimensions can't be sniffed (unknown/SVG/corrupt) do we keep the
1955
+ # legacy fixed 6" x 4.5" fallback.
1956
+ dims_emu = scaled_dimensions_emu(
1957
+ image_bytes,
1958
+ int(width) if width is not None else None,
1959
+ int(height) if height is not None else None,
1960
+ )
1961
+ if dims_emu is not None:
1962
+ w_emu, h_emu = dims_emu
1963
+ else:
1964
+ # Undecodable native size — legacy 6" x 4.5" fixed fallback
1965
+ # (576px / 432px @ 96 DPI) or the explicit dimension.
1966
+ w_emu = int(width) if width is not None else 5486400
1967
+ h_emu = int(height) if height is not None else 4114800
1968
+ # The ``create.image`` wire wants pixels (96 DPI); the returned
1969
+ # InlineShape's ``size`` is read back as POINTS (the getter wraps
1970
+ # it in ``Pt(...)``, matching the resize setter's ``unit: "pt"``),
1971
+ # so store the two units separately — otherwise ``shape.width``
1972
+ # reads back ~1.33x inflated (px interpreted as pt).
1973
+ w_px: float = _emu_to_px(w_emu)
1974
+ h_px: float = _emu_to_px(h_emu)
1975
+ w_pt: float = w_emu / 12700.0
1976
+ h_pt: float = h_emu / 12700.0
1936
1977
 
1937
1978
  # Pre-mint the client-side UUID so the create can buffer and
1938
1979
  # the InlineShape proxy hands the caller a stable id without
@@ -1948,7 +1989,7 @@ class Document:
1948
1989
  # InlineShape stores its id under ``_info["nodeId"]`` rather than
1949
1990
  # a top-level ``_node_id`` attribute, so route the flush-time
1950
1991
  # rewrite through a small setattr-adapter wrapper.
1951
- info = _build_inline_shape_info(result, width=w_px, height=h_px)
1992
+ info = _build_inline_shape_info(result, width=w_pt, height=h_pt)
1952
1993
  info["nodeId"] = client_node_id
1953
1994
  shape = InlineShape(session=self._session, info=info)
1954
1995
  buffer = self._buffer()
@@ -1981,6 +2022,22 @@ class Document:
1981
2022
  if start_type is not None:
1982
2023
  if isinstance(start_type, WD_SECTION_START):
1983
2024
  break_type = start_type.to_superdoc()
2025
+ elif isinstance(start_type, int):
2026
+ # python-docx's WD_SECTION_START members ARE ints
2027
+ # (NEW_PAGE == 2), so ``add_section(2)`` is valid upstream.
2028
+ # Athena's members carry SuperDoc string values, so map the
2029
+ # upstream integer constants to the matching member.
2030
+ _int_to_member = {
2031
+ 0: WD_SECTION_START.CONTINUOUS,
2032
+ 1: WD_SECTION_START.NEW_COLUMN,
2033
+ 2: WD_SECTION_START.NEW_PAGE,
2034
+ 3: WD_SECTION_START.EVEN_PAGE,
2035
+ 4: WD_SECTION_START.ODD_PAGE,
2036
+ }
2037
+ member = _int_to_member.get(int(start_type))
2038
+ if member is None:
2039
+ raise ValueError(f"{start_type!r} is not a valid WD_SECTION_START")
2040
+ break_type = member.to_superdoc()
1984
2041
  elif isinstance(start_type, str):
1985
2042
  # python-docx coerces strings through the enum, raising
1986
2043
  # ``ValueError`` for non-member values. Mirror that here