athena-python-docx 0.11.2__tar.gz → 0.11.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/PKG-INFO +1 -1
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/__init__.py +1 -1
- athena_python_docx-0.11.3/docx/oxml/__init__.py +216 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/pyproject.toml +1 -1
- athena_python_docx-0.11.3/tests/test_oxml_shim.py +174 -0
- athena_python_docx-0.11.2/docx/oxml/__init__.py +0 -148
- athena_python_docx-0.11.2/tests/test_oxml_shim.py +0 -123
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/.gitignore +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/CLAUDE.md +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/README.md +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/_athena_extension.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/_batching.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/_buffer.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/_http.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/_http_doc.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/_image_utils.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/_ptc.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/_table_styles.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/api.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/bookmarks.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/charts.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/client.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/commands.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/comments.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/document.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/enum/section.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/enum/style.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/enum/table.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/enum/text.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/errors.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/exceptions.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/fields.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/footnotes.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/math.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/opc/coreprops.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/revisions.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/sdt.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/section.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/settings.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/shape.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/shared.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/styles/style.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/styles/styles.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/table.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/text/font.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/text/hyperlink.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/text/pagebreak.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/text/paragraph.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/text/parfmt.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/text/run.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/text/tabstops.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/toc.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/docx/typing.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/scripts/publish.sh +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/scripts/release.sh +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/scripts/round_trip_smoke.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/scripts/smoke_test_block_not_found.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/__init__.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/conftest.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/METHODOLOGY.md +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/README.md +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/ab_probe_cases.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/ab_probe_runner.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/binary_round_trip.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/fake_session.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/firm_templates/README.md +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/firm_templates/__init__.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/firm_templates/_runner.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/firm_templates/extractor.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/firm_templates/test_pw_corpus.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/firm_templates/test_pw_research_digest.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/local_runner.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshot.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/ours_spec.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/parity_diff.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/real_world_cases.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/runner.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/stock_spec.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/fidelity/test_e2e_against_staging.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/README.md +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/__init__.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/baseline_gaps.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/compare.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/intentional_deviations.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/introspect.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/reports/GAP_ANALYSIS.md +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/reports/gap_report.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/run_parity.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/snapshots/athena_latest.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/parity/test_parity_gap.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_athena_extensions_contract.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_athena_extensions_registry.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_batching_perf.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_block_not_found_error.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_buffer.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_cell_add_paragraph_wire_shape.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_cell_add_table_not_supported.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_cell_text_plain_fastpath.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_collapsed_range_format.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_command_dataclasses.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_commands.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_comments.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_document_asset_id_property.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_document_create.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_document_create_from_template.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_document_factory_validation.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_e2e_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_http_transport.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_hyperlink_coalescing.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_insert_deferred.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_iter_inner_content.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_list_styles.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_merged_cell_secondary_slot.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_merged_cells.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_paragraph_text_len_cache.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_parity_misc.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_parity_round2.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_phase_a_behavior.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_phase_b_headers_footers.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_phase_c_tables.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_pr19766_review_fixes.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_ptc.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_revisions.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_add_paragraph_style.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_add_picture.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_add_run.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_comments_add_comment.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_comments_get.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_document_audit.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_document_element.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_enum_section.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_font_audit.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_header_footer.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_hyperlink.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_inline_shape.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_misc.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_paragraph_strict.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_paragraph_style.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_paragraph_style_strict.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_parfmt.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_row_col_cell.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_run_add_break.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_run_bool_setters.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_run_style.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_run_style_strict.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_run_text.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_run_underline.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_section_audit.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_section_dimensions.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_section_onoff.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_settings.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_shared_audit.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_style.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_styles.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_table_audit.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_table_cell.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_table_dimensions.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_silent_stub_table_layout.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_smoke_integration.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_style_acceptance.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_style_font.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_style_setters_contract.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_table_set_cell_perf.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_table_style_id_resolution.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_temporarily_unavailable.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_wire_contract.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/tests/test_zod_wire_contract.py +0 -0
- {athena_python_docx-0.11.2 → athena_python_docx-0.11.3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.3
|
|
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>
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Stub package — ``docx.oxml`` is intentionally unavailable in
|
|
2
|
+
athena-python-docx.
|
|
3
|
+
|
|
4
|
+
python-docx upstream exposes raw OOXML element classes (``OxmlElement``,
|
|
5
|
+
``CT_*``, ``qn``, ``nsmap``, …) for advanced users who manipulate
|
|
6
|
+
Word's XML directly. athena-python-docx is backed by a SuperDoc Y.Doc
|
|
7
|
+
+ Keryx pipeline, not a local OOXML tree, so there is no XML element
|
|
8
|
+
to manipulate.
|
|
9
|
+
|
|
10
|
+
Before 0.8.0 attempts to ``from docx.oxml.ns import qn`` failed with
|
|
11
|
+
the stdlib's generic ``ModuleNotFoundError: No module named
|
|
12
|
+
'docx.oxml'`` — agent code had to discover the gap by retry. 0.8.0
|
|
13
|
+
turned this package into a typed stub, but the stub raised
|
|
14
|
+
``OxmlNotAvailableError`` on attribute access — which is what
|
|
15
|
+
``from docx.oxml.ns import qn`` triggers, so the import statement
|
|
16
|
+
itself crashed before any line of the agent's actual logic ran. Common
|
|
17
|
+
python-docx convention is to declare ``qn`` / ``OxmlElement`` at the
|
|
18
|
+
top of every script "just in case," and that preamble killed scripts
|
|
19
|
+
that only used the high-level API.
|
|
20
|
+
|
|
21
|
+
As of 0.11.3 the stub is **lazy**: ``from docx.oxml.ns import qn``
|
|
22
|
+
succeeds and returns a sentinel proxy that defers the typed error
|
|
23
|
+
until the symbol is actually called or further-attribute-accessed.
|
|
24
|
+
Scripts that import for parity but never use the symbol now run
|
|
25
|
+
cleanly; scripts that reach for ``qn("w:tcW")`` still get the
|
|
26
|
+
actionable error pointing at the high-level alternative.
|
|
27
|
+
|
|
28
|
+
The gap is documented in ``docx-studio/python-sdk/CLAUDE.md`` § "Intentionally
|
|
29
|
+
omitted" and locked into the parity matrix via
|
|
30
|
+
``tests/parity/intentional_deviations.json`` (``docx.oxml**``).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import sys
|
|
36
|
+
import types
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OxmlNotAvailableError(ImportError):
|
|
41
|
+
"""Raised when a ``docx.oxml.*`` symbol is actually used.
|
|
42
|
+
|
|
43
|
+
Inherits :class:`ImportError` so callers using
|
|
44
|
+
``try: from docx.oxml import X / except ImportError`` still match
|
|
45
|
+
after 0.11.3's lazy-stub change (the import itself succeeds, but
|
|
46
|
+
the first call site against the imported name raises this — and
|
|
47
|
+
code that uses ``except ImportError`` around a use-site still
|
|
48
|
+
catches it).
|
|
49
|
+
|
|
50
|
+
Use ``except OxmlNotAvailableError`` to distinguish a "no oxml
|
|
51
|
+
surface" miss from a real import-resolution failure or other
|
|
52
|
+
``ImportError`` cause.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_MESSAGE: str = (
|
|
57
|
+
"docx.oxml is not available in athena-python-docx — the SuperDoc "
|
|
58
|
+
"backend doesn't expose raw OOXML elements. Use the high-level "
|
|
59
|
+
"python-docx API (Document.add_paragraph, Run.bold, _Cell.text, …) "
|
|
60
|
+
"or the typed command surface in docx.commands for any mutation "
|
|
61
|
+
"the high-level API doesn't cover. "
|
|
62
|
+
"See docx-studio/python-sdk/CLAUDE.md § 'Intentionally omitted' for "
|
|
63
|
+
"the parity rationale."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _LazyRaisingProxy:
|
|
68
|
+
"""Sentinel returned by the ``docx.oxml`` stub for any imported name.
|
|
69
|
+
|
|
70
|
+
Behaves as a callable, an attribute-accessible namespace, and an
|
|
71
|
+
awaitable for the union of common python-docx patterns:
|
|
72
|
+
|
|
73
|
+
* ``from docx.oxml.ns import qn`` → ``qn`` is a _LazyRaisingProxy.
|
|
74
|
+
Just importing it is a no-op. ``qn("w:tcW")`` raises with the
|
|
75
|
+
educated message.
|
|
76
|
+
* ``from docx.oxml import OxmlElement`` → same. ``OxmlElement(...)``
|
|
77
|
+
raises; ``OxmlElement.something`` raises.
|
|
78
|
+
* ``isinstance(x, OxmlElement)`` → raises (we'd rather a noisy
|
|
79
|
+
failure than silently say no, since the typed error tells the
|
|
80
|
+
caller what to do).
|
|
81
|
+
|
|
82
|
+
The proxy carries the full dotted path so the error message points
|
|
83
|
+
at exactly the offending symbol — ``docx.oxml.ns.qn`` rather than a
|
|
84
|
+
generic "docx.oxml".
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
__slots__ = ("_path",)
|
|
88
|
+
|
|
89
|
+
def __init__(self, path: str) -> None:
|
|
90
|
+
# Bypass __setattr__ guard.
|
|
91
|
+
object.__setattr__(self, "_path", path)
|
|
92
|
+
|
|
93
|
+
def _raise(self, suffix: str = "") -> None:
|
|
94
|
+
path = object.__getattribute__(self, "_path")
|
|
95
|
+
target = f"{path}{suffix}" if suffix else path
|
|
96
|
+
raise OxmlNotAvailableError(f"{target} — {_MESSAGE}")
|
|
97
|
+
|
|
98
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
99
|
+
self._raise()
|
|
100
|
+
|
|
101
|
+
def __getattr__(self, name: str) -> Any: # noqa: ANN401
|
|
102
|
+
if name.startswith("__") and name.endswith("__"):
|
|
103
|
+
raise AttributeError(name)
|
|
104
|
+
self._raise(f".{name}")
|
|
105
|
+
|
|
106
|
+
def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401
|
|
107
|
+
# Block writes to the proxy so agents that try
|
|
108
|
+
# ``OxmlElement.tag = "..."`` get the typed error rather than a
|
|
109
|
+
# silent attribute set on the sentinel.
|
|
110
|
+
if name == "_path":
|
|
111
|
+
object.__setattr__(self, name, value)
|
|
112
|
+
return
|
|
113
|
+
self._raise(f".{name}")
|
|
114
|
+
|
|
115
|
+
def __repr__(self) -> str:
|
|
116
|
+
path = object.__getattribute__(self, "_path")
|
|
117
|
+
return f"<docx.oxml lazy stub {path!r} — call to raise>"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _OxmlStubModule(types.ModuleType):
|
|
121
|
+
"""ModuleType subclass that returns :class:`_LazyRaisingProxy`
|
|
122
|
+
sentinels for any attribute access.
|
|
123
|
+
|
|
124
|
+
Installed into :data:`sys.modules` at package-init time so
|
|
125
|
+
qualified imports like ``from docx.oxml.ns import qn`` find a real
|
|
126
|
+
module object (avoiding ``ModuleNotFoundError``). The proxy
|
|
127
|
+
returned defers the typed error until the symbol is actually
|
|
128
|
+
invoked, letting top-level python-docx import blocks succeed.
|
|
129
|
+
|
|
130
|
+
No ``__slots__`` — ``types.ModuleType`` itself carries a
|
|
131
|
+
``__dict__``, so a slots declaration on a subclass is a no-op.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __getattr__(self, name: str) -> Any: # noqa: ANN401
|
|
135
|
+
if name.startswith("__") and name.endswith("__"):
|
|
136
|
+
# Dunder access (e.g. ``__path__`` during submodule import)
|
|
137
|
+
# must fall through to AttributeError so Python's import
|
|
138
|
+
# machinery can probe without triggering our typed error.
|
|
139
|
+
raise AttributeError(name)
|
|
140
|
+
path = self.__name__
|
|
141
|
+
proxy = _LazyRaisingProxy(f"{path}.{name}")
|
|
142
|
+
# Cache so repeated `getattr` returns the same proxy and so
|
|
143
|
+
# `from X import Y` followed by another `from X import Y`
|
|
144
|
+
# gives the same instance.
|
|
145
|
+
object.__setattr__(self, name, proxy)
|
|
146
|
+
return proxy
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Common upstream submodule paths. Listed explicitly rather than wild-
|
|
150
|
+
# carded so adding a new one is a visible diff. Every entry maps to a
|
|
151
|
+
# single sentinel module; no per-submodule state.
|
|
152
|
+
_SUBMODULES: tuple[str, ...] = (
|
|
153
|
+
"ns",
|
|
154
|
+
"parser",
|
|
155
|
+
"element",
|
|
156
|
+
"xmlchemy",
|
|
157
|
+
"exceptions",
|
|
158
|
+
"ooxml",
|
|
159
|
+
"coreprops",
|
|
160
|
+
"document",
|
|
161
|
+
"text",
|
|
162
|
+
"table",
|
|
163
|
+
"comments",
|
|
164
|
+
"header_footer",
|
|
165
|
+
"footnote",
|
|
166
|
+
"endnote",
|
|
167
|
+
"shared",
|
|
168
|
+
"shape",
|
|
169
|
+
"section",
|
|
170
|
+
"settings",
|
|
171
|
+
"styles",
|
|
172
|
+
"numbering",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _install_stub_submodules() -> None:
|
|
177
|
+
"""Pre-populate :data:`sys.modules` with stub entries for every
|
|
178
|
+
well-known ``docx.oxml.*`` submodule.
|
|
179
|
+
|
|
180
|
+
Idempotent — re-running is a no-op for entries already installed
|
|
181
|
+
(covers the rare case of a test that reloads the package).
|
|
182
|
+
"""
|
|
183
|
+
for short in _SUBMODULES:
|
|
184
|
+
full = f"docx.oxml.{short}"
|
|
185
|
+
if full in sys.modules:
|
|
186
|
+
continue
|
|
187
|
+
sys.modules[full] = _OxmlStubModule(full)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
_install_stub_submodules()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def __getattr__(name: str) -> Any: # noqa: ANN401
|
|
194
|
+
"""PEP 562 attribute hook for ``from docx.oxml import X``.
|
|
195
|
+
|
|
196
|
+
Submodule names (handled via :data:`sys.modules` injection in
|
|
197
|
+
:func:`_install_stub_submodules`) and dunder names fall through
|
|
198
|
+
normally. Everything else returns a :class:`_LazyRaisingProxy` so
|
|
199
|
+
the import succeeds and the typed error fires only on actual use.
|
|
200
|
+
"""
|
|
201
|
+
if name.startswith("__") and name.endswith("__"):
|
|
202
|
+
raise AttributeError(name)
|
|
203
|
+
if name in _SUBMODULES:
|
|
204
|
+
# Should already be present in sys.modules; defensive fallback
|
|
205
|
+
# so we never leak ModuleNotFoundError if the caller reloaded
|
|
206
|
+
# the package between calls.
|
|
207
|
+
full = f"docx.oxml.{name}"
|
|
208
|
+
mod = sys.modules.get(full)
|
|
209
|
+
if mod is None:
|
|
210
|
+
mod = _OxmlStubModule(full)
|
|
211
|
+
sys.modules[full] = mod
|
|
212
|
+
return mod
|
|
213
|
+
return _LazyRaisingProxy(f"docx.oxml.{name}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = ["OxmlNotAvailableError"]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "athena-python-docx"
|
|
7
|
-
version = "0.11.
|
|
7
|
+
version = "0.11.3"
|
|
8
8
|
description = "Drop-in replacement for python-docx that connects to Athena's Superdoc/Keryx collaborative document stack"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Lock-in tests for the ``docx.oxml`` stub package.
|
|
2
|
+
|
|
3
|
+
``docx.oxml`` is intentionally unavailable in athena-python-docx —
|
|
4
|
+
agent code that imports ``docx.oxml.ns``, ``docx.oxml.parser``, etc.
|
|
5
|
+
used to fail with the stdlib's generic ``ModuleNotFoundError: No
|
|
6
|
+
module named 'docx.oxml'`` and had to retry to discover the gap.
|
|
7
|
+
|
|
8
|
+
The 0.8.0 stub package replaced that with a single typed
|
|
9
|
+
:class:`OxmlNotAvailableError` that named the high-level python-docx
|
|
10
|
+
surface the caller should use instead — but the 0.8.0 stub raised
|
|
11
|
+
on attribute access, which is what ``from docx.oxml.ns import qn``
|
|
12
|
+
triggers. That meant the import statement itself crashed before any
|
|
13
|
+
line of the agent's actual logic ran. The common python-docx
|
|
14
|
+
convention of declaring ``qn`` / ``OxmlElement`` at the top of every
|
|
15
|
+
script "just in case" killed scripts that only used the high-level
|
|
16
|
+
API.
|
|
17
|
+
|
|
18
|
+
As of 0.11.3 the stub is **lazy**:
|
|
19
|
+
|
|
20
|
+
1. Every common upstream submodule (``ns``, ``parser``, ``element``,
|
|
21
|
+
``xmlchemy``, …) is importable — same as 0.8.0.
|
|
22
|
+
2. Attribute access on those modules now returns a sentinel proxy
|
|
23
|
+
rather than raising — ``from docx.oxml.ns import qn`` succeeds,
|
|
24
|
+
``qn`` is bound to a :class:`_LazyRaisingProxy`.
|
|
25
|
+
3. The typed :class:`OxmlNotAvailableError` fires only when the
|
|
26
|
+
imported symbol is actually used — called (``qn(...)``),
|
|
27
|
+
attribute-accessed (``OxmlElement.tag``), or written-to
|
|
28
|
+
(``OxmlElement.tag = "..."``).
|
|
29
|
+
|
|
30
|
+
These tests pin all three contracts so a future refactor can't
|
|
31
|
+
regress to the 0.8.0 import-time failure or to a totally-permissive
|
|
32
|
+
no-op stub.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import importlib
|
|
38
|
+
import pytest
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_oxml_not_available_error_is_importerror_subclass() -> None:
|
|
42
|
+
"""Code that uses ``try / except ImportError`` to detect missing
|
|
43
|
+
oxml support must still catch :class:`OxmlNotAvailableError`."""
|
|
44
|
+
from docx.oxml import OxmlNotAvailableError
|
|
45
|
+
|
|
46
|
+
assert issubclass(OxmlNotAvailableError, ImportError)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize(
|
|
50
|
+
"submodule",
|
|
51
|
+
[
|
|
52
|
+
"ns",
|
|
53
|
+
"parser",
|
|
54
|
+
"element",
|
|
55
|
+
"xmlchemy",
|
|
56
|
+
"exceptions",
|
|
57
|
+
"coreprops",
|
|
58
|
+
"document",
|
|
59
|
+
"text",
|
|
60
|
+
"table",
|
|
61
|
+
"comments",
|
|
62
|
+
"header_footer",
|
|
63
|
+
"section",
|
|
64
|
+
"settings",
|
|
65
|
+
"styles",
|
|
66
|
+
"numbering",
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
def test_submodule_import_succeeds(submodule: str) -> None:
|
|
70
|
+
"""``import docx.oxml.<X>`` must resolve to a stub module without
|
|
71
|
+
raising. Same contract as 0.8.0."""
|
|
72
|
+
mod = importlib.import_module(f"docx.oxml.{submodule}")
|
|
73
|
+
assert mod is not None
|
|
74
|
+
assert mod.__name__ == f"docx.oxml.{submodule}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_top_level_import_succeeds_lazy() -> None:
|
|
78
|
+
"""``from docx.oxml import OxmlElement`` (and friends) must
|
|
79
|
+
succeed and bind a sentinel proxy. The 0.8.0 stub raised at
|
|
80
|
+
import time; 0.11.3 defers the error until the symbol is used.
|
|
81
|
+
"""
|
|
82
|
+
from docx.oxml import OxmlElement # must NOT raise
|
|
83
|
+
|
|
84
|
+
assert OxmlElement is not None
|
|
85
|
+
# repr should reveal the lazy nature so debugging is easy.
|
|
86
|
+
assert "docx.oxml.OxmlElement" in repr(OxmlElement)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_submodule_import_succeeds_lazy() -> None:
|
|
90
|
+
"""``from docx.oxml.ns import qn`` must succeed and bind a sentinel
|
|
91
|
+
proxy. This is the most common python-docx import pattern."""
|
|
92
|
+
from docx.oxml.ns import qn, nsmap # must NOT raise
|
|
93
|
+
|
|
94
|
+
assert qn is not None
|
|
95
|
+
assert nsmap is not None
|
|
96
|
+
assert "docx.oxml.ns.qn" in repr(qn)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_call_on_imported_symbol_raises() -> None:
|
|
100
|
+
"""The typed error fires when the imported symbol is actually
|
|
101
|
+
invoked. ``qn("w:tcW")`` raises :class:`OxmlNotAvailableError`
|
|
102
|
+
with the educated message pointing at the high-level surface."""
|
|
103
|
+
from docx.oxml import OxmlNotAvailableError
|
|
104
|
+
from docx.oxml.ns import qn
|
|
105
|
+
|
|
106
|
+
with pytest.raises(OxmlNotAvailableError) as exc_info:
|
|
107
|
+
qn("w:tcW")
|
|
108
|
+
msg = str(exc_info.value)
|
|
109
|
+
assert "docx.oxml.ns.qn" in msg
|
|
110
|
+
assert "Document.add_paragraph" in msg
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_attribute_access_on_imported_symbol_raises() -> None:
|
|
114
|
+
"""``OxmlElement.tag`` and similar attribute lookups on the
|
|
115
|
+
sentinel proxy raise the typed error with the full dotted path."""
|
|
116
|
+
from docx.oxml import OxmlElement, OxmlNotAvailableError
|
|
117
|
+
|
|
118
|
+
with pytest.raises(OxmlNotAvailableError) as exc_info:
|
|
119
|
+
_ = OxmlElement.tag
|
|
120
|
+
msg = str(exc_info.value)
|
|
121
|
+
assert "docx.oxml.OxmlElement.tag" in msg
|
|
122
|
+
assert "Document.add_paragraph" in msg
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_attribute_write_on_imported_symbol_raises() -> None:
|
|
126
|
+
"""Writes against the sentinel (``OxmlElement.tag = "..."``) also
|
|
127
|
+
raise — otherwise the assignment silently succeeds and the next
|
|
128
|
+
read of ``OxmlElement.tag`` returns the cached value, masking the
|
|
129
|
+
fact that no XML element actually exists."""
|
|
130
|
+
from docx.oxml import OxmlElement, OxmlNotAvailableError
|
|
131
|
+
|
|
132
|
+
with pytest.raises(OxmlNotAvailableError):
|
|
133
|
+
OxmlElement.tag = "w:foo"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_call_on_module_level_symbol_raises() -> None:
|
|
137
|
+
"""``oxml.OxmlElement(...)`` (without a from-import) follows the
|
|
138
|
+
same path — the package-level ``__getattr__`` returns the lazy
|
|
139
|
+
proxy, the proxy raises on call."""
|
|
140
|
+
import docx.oxml as oxml
|
|
141
|
+
from docx.oxml import OxmlNotAvailableError
|
|
142
|
+
|
|
143
|
+
with pytest.raises(OxmlNotAvailableError):
|
|
144
|
+
oxml.OxmlElement()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_repeated_attribute_access_returns_same_proxy() -> None:
|
|
148
|
+
"""``ns.qn`` accessed twice returns the same proxy instance so
|
|
149
|
+
callers that bind it to a local then re-fetch (e.g. assignments
|
|
150
|
+
in different functions) don't get diverging sentinels."""
|
|
151
|
+
import docx.oxml.ns as ns
|
|
152
|
+
|
|
153
|
+
first = ns.qn
|
|
154
|
+
second = ns.qn
|
|
155
|
+
assert first is second
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_dunder_attribute_access_falls_through() -> None:
|
|
159
|
+
"""Dunder access (``__path__``, ``__loader__``, etc.) must NOT
|
|
160
|
+
return a proxy — the import machinery probes those during
|
|
161
|
+
submodule resolution. Treating them as user attribute access
|
|
162
|
+
would break the import path."""
|
|
163
|
+
import docx.oxml.ns as ns
|
|
164
|
+
|
|
165
|
+
with pytest.raises(AttributeError):
|
|
166
|
+
_ = ns.__nonexistent_dunder__
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_from_submodule_import_succeeds_lazy() -> None:
|
|
170
|
+
"""``from docx.oxml.parser import OxmlElement`` must succeed
|
|
171
|
+
under the lazy contract — 0.8.0 raised here, but the agent's
|
|
172
|
+
actual mistake is the call site, not the import."""
|
|
173
|
+
# Should not raise.
|
|
174
|
+
exec("from docx.oxml.parser import OxmlElement", {})
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"""Stub package — ``docx.oxml`` is intentionally unavailable in
|
|
2
|
-
athena-python-docx.
|
|
3
|
-
|
|
4
|
-
python-docx upstream exposes raw OOXML element classes (``OxmlElement``,
|
|
5
|
-
``CT_*``, ``qn``, ``nsmap``, …) for advanced users who manipulate
|
|
6
|
-
Word's XML directly. athena-python-docx is backed by a SuperDoc Y.Doc
|
|
7
|
-
+ Keryx pipeline, not a local OOXML tree, so there is no XML element
|
|
8
|
-
to manipulate.
|
|
9
|
-
|
|
10
|
-
Before 0.8.0 attempts to ``from docx.oxml.ns import qn`` failed with
|
|
11
|
-
the stdlib's generic ``ModuleNotFoundError: No module named
|
|
12
|
-
'docx.oxml'`` — agent code had to discover the gap by retry. As of
|
|
13
|
-
0.8.0 this package is a typed stub: every import path resolves, but
|
|
14
|
-
every attribute access raises :class:`OxmlNotAvailableError` with a
|
|
15
|
-
single actionable message pointing at the high-level python-docx
|
|
16
|
-
surface (``Document.add_paragraph``, ``Run.bold``, ``_Cell.text``, …)
|
|
17
|
-
or the typed command bus (``docx.commands``).
|
|
18
|
-
|
|
19
|
-
The gap is documented in ``docx-studio/python-sdk/CLAUDE.md`` § "Intentionally
|
|
20
|
-
omitted" and locked into the parity matrix via
|
|
21
|
-
``tests/parity/intentional_deviations.json`` (``docx.oxml**``).
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import sys
|
|
27
|
-
import types
|
|
28
|
-
from typing import Any
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class OxmlNotAvailableError(ImportError):
|
|
32
|
-
"""Raised on any access to ``docx.oxml.*`` symbols.
|
|
33
|
-
|
|
34
|
-
Inherits :class:`ImportError` so callers using
|
|
35
|
-
``try: from docx.oxml import X / except ImportError`` still match,
|
|
36
|
-
while ``except OxmlNotAvailableError`` lets newer code distinguish
|
|
37
|
-
a "no oxml surface" miss from a real import-resolution failure.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
_MESSAGE: str = (
|
|
42
|
-
"docx.oxml is not available in athena-python-docx — the SuperDoc "
|
|
43
|
-
"backend doesn't expose raw OOXML elements. Use the high-level "
|
|
44
|
-
"python-docx API (Document.add_paragraph, Run.bold, _Cell.text, …) "
|
|
45
|
-
"or the typed command surface in docx.commands for any mutation "
|
|
46
|
-
"the high-level API doesn't cover. "
|
|
47
|
-
"See docx-studio/python-sdk/CLAUDE.md § 'Intentionally omitted' for "
|
|
48
|
-
"the parity rationale."
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class _OxmlStubModule(types.ModuleType):
|
|
53
|
-
"""ModuleType subclass that raises :class:`OxmlNotAvailableError`
|
|
54
|
-
on every attribute access.
|
|
55
|
-
|
|
56
|
-
Installed into :data:`sys.modules` at package-init time so
|
|
57
|
-
qualified imports like ``from docx.oxml.ns import qn`` find a real
|
|
58
|
-
module object (avoiding ``ModuleNotFoundError``) and raise the
|
|
59
|
-
typed exception when the symbol is dereferenced.
|
|
60
|
-
|
|
61
|
-
No ``__slots__`` — ``types.ModuleType`` itself carries a
|
|
62
|
-
``__dict__``, so a slots declaration on a subclass is a no-op and
|
|
63
|
-
doesn't prevent ``docx.oxml.ns.qn = something`` from silently
|
|
64
|
-
bypassing :meth:`__getattr__` on the next read. If a future caller
|
|
65
|
-
needs to block that bypass we'd need a ``__setattr__`` guard that
|
|
66
|
-
rejects user-attribute writes while still letting the import
|
|
67
|
-
machinery install dunders (``__loader__``, ``__spec__``, …) on the
|
|
68
|
-
stub at package-init time.
|
|
69
|
-
"""
|
|
70
|
-
|
|
71
|
-
def __getattr__(self, name: str) -> Any: # noqa: ANN401
|
|
72
|
-
if name.startswith("__") and name.endswith("__"):
|
|
73
|
-
# Dunder access (e.g. ``__path__`` during submodule import)
|
|
74
|
-
# must fall through to AttributeError so Python's import
|
|
75
|
-
# machinery can probe without triggering our typed error.
|
|
76
|
-
raise AttributeError(name)
|
|
77
|
-
path = self.__name__
|
|
78
|
-
raise OxmlNotAvailableError(f"{path}.{name} — {_MESSAGE}")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# Common upstream submodule paths. Listed explicitly rather than wild-
|
|
82
|
-
# carded so adding a new one is a visible diff. Every entry maps to a
|
|
83
|
-
# single sentinel module; no per-submodule state.
|
|
84
|
-
_SUBMODULES: tuple[str, ...] = (
|
|
85
|
-
"ns",
|
|
86
|
-
"parser",
|
|
87
|
-
"element",
|
|
88
|
-
"xmlchemy",
|
|
89
|
-
"exceptions",
|
|
90
|
-
"ooxml",
|
|
91
|
-
"coreprops",
|
|
92
|
-
"document",
|
|
93
|
-
"text",
|
|
94
|
-
"table",
|
|
95
|
-
"comments",
|
|
96
|
-
"header_footer",
|
|
97
|
-
"footnote",
|
|
98
|
-
"endnote",
|
|
99
|
-
"shared",
|
|
100
|
-
"shape",
|
|
101
|
-
"section",
|
|
102
|
-
"settings",
|
|
103
|
-
"styles",
|
|
104
|
-
"numbering",
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _install_stub_submodules() -> None:
|
|
109
|
-
"""Pre-populate :data:`sys.modules` with stub entries for every
|
|
110
|
-
well-known ``docx.oxml.*`` submodule.
|
|
111
|
-
|
|
112
|
-
Idempotent — re-running is a no-op for entries already installed
|
|
113
|
-
(covers the rare case of a test that reloads the package).
|
|
114
|
-
"""
|
|
115
|
-
for short in _SUBMODULES:
|
|
116
|
-
full = f"docx.oxml.{short}"
|
|
117
|
-
if full in sys.modules:
|
|
118
|
-
continue
|
|
119
|
-
sys.modules[full] = _OxmlStubModule(full)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
_install_stub_submodules()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def __getattr__(name: str) -> Any: # noqa: ANN401
|
|
126
|
-
"""PEP 562 attribute hook for ``from docx.oxml import X``.
|
|
127
|
-
|
|
128
|
-
Submodule names (handled via :data:`sys.modules` injection in
|
|
129
|
-
:func:`_install_stub_submodules`) and dunder names fall through to
|
|
130
|
-
the default ``AttributeError`` so Python's import machinery
|
|
131
|
-
behaves normally. Everything else raises the typed error.
|
|
132
|
-
"""
|
|
133
|
-
if name.startswith("__") and name.endswith("__"):
|
|
134
|
-
raise AttributeError(name)
|
|
135
|
-
if name in _SUBMODULES:
|
|
136
|
-
# Should already be present in sys.modules; defensive fallback
|
|
137
|
-
# so we never leak ModuleNotFoundError if the caller reloaded
|
|
138
|
-
# the package between calls.
|
|
139
|
-
full = f"docx.oxml.{name}"
|
|
140
|
-
mod = sys.modules.get(full)
|
|
141
|
-
if mod is None:
|
|
142
|
-
mod = _OxmlStubModule(full)
|
|
143
|
-
sys.modules[full] = mod
|
|
144
|
-
return mod
|
|
145
|
-
raise OxmlNotAvailableError(f"docx.oxml.{name} — {_MESSAGE}")
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
__all__ = ["OxmlNotAvailableError"]
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
"""Lock-in tests for the ``docx.oxml`` stub package.
|
|
2
|
-
|
|
3
|
-
``docx.oxml`` is intentionally unavailable in athena-python-docx —
|
|
4
|
-
agent code that imports ``docx.oxml.ns``, ``docx.oxml.parser``, etc.
|
|
5
|
-
used to fail with the stdlib's generic ``ModuleNotFoundError: No
|
|
6
|
-
module named 'docx.oxml'`` and had to retry to discover the gap. The
|
|
7
|
-
0.8.0 stub package replaces that with a single typed
|
|
8
|
-
:class:`OxmlNotAvailableError` that names the high-level python-docx
|
|
9
|
-
surface the caller should use instead, so the agent learns
|
|
10
|
-
immediately.
|
|
11
|
-
|
|
12
|
-
These tests pin both halves of the contract:
|
|
13
|
-
|
|
14
|
-
1. Every common upstream submodule (``ns``, ``parser``, ``element``,
|
|
15
|
-
``xmlchemy``, etc.) is importable — i.e. the import doesn't raise
|
|
16
|
-
``ModuleNotFoundError``.
|
|
17
|
-
2. Any attribute access on those modules raises
|
|
18
|
-
:class:`OxmlNotAvailableError` with the actionable message pointing
|
|
19
|
-
at the high-level API.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
from __future__ import annotations
|
|
23
|
-
|
|
24
|
-
import importlib
|
|
25
|
-
import pytest
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_package_attribute_access_raises_typed_error() -> None:
|
|
29
|
-
"""``from docx.oxml import OxmlElement`` (and friends) must raise
|
|
30
|
-
:class:`OxmlNotAvailableError`, not the stdlib's bare
|
|
31
|
-
``ImportError`` or ``AttributeError``."""
|
|
32
|
-
import docx.oxml as oxml
|
|
33
|
-
from docx.oxml import OxmlNotAvailableError
|
|
34
|
-
|
|
35
|
-
with pytest.raises(OxmlNotAvailableError) as exc_info:
|
|
36
|
-
_ = oxml.OxmlElement
|
|
37
|
-
assert "OxmlElement" in str(exc_info.value)
|
|
38
|
-
assert "docx.oxml" in str(exc_info.value)
|
|
39
|
-
# The actionable hint must name the high-level surface.
|
|
40
|
-
assert "Document.add_paragraph" in str(exc_info.value)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_oxml_not_available_error_is_importerror_subclass() -> None:
|
|
44
|
-
"""Code that uses ``try / except ImportError`` to detect missing
|
|
45
|
-
oxml support must still catch :class:`OxmlNotAvailableError` so
|
|
46
|
-
pre-0.8.0 callers don't regress."""
|
|
47
|
-
from docx.oxml import OxmlNotAvailableError
|
|
48
|
-
|
|
49
|
-
assert issubclass(OxmlNotAvailableError, ImportError)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@pytest.mark.parametrize(
|
|
53
|
-
"submodule",
|
|
54
|
-
[
|
|
55
|
-
"ns",
|
|
56
|
-
"parser",
|
|
57
|
-
"element",
|
|
58
|
-
"xmlchemy",
|
|
59
|
-
"exceptions",
|
|
60
|
-
"coreprops",
|
|
61
|
-
"document",
|
|
62
|
-
"text",
|
|
63
|
-
"table",
|
|
64
|
-
"comments",
|
|
65
|
-
"header_footer",
|
|
66
|
-
"section",
|
|
67
|
-
"settings",
|
|
68
|
-
"styles",
|
|
69
|
-
"numbering",
|
|
70
|
-
],
|
|
71
|
-
)
|
|
72
|
-
def test_submodule_import_succeeds(submodule: str) -> None:
|
|
73
|
-
"""``import docx.oxml.<X>`` must resolve to a stub module without
|
|
74
|
-
raising. The stub raises on attribute access, but the import
|
|
75
|
-
itself succeeds — that's the contract that distinguishes a stub
|
|
76
|
-
from a totally-missing module (which would still raise
|
|
77
|
-
``ModuleNotFoundError`` and force agents to retry)."""
|
|
78
|
-
mod = importlib.import_module(f"docx.oxml.{submodule}")
|
|
79
|
-
assert mod is not None
|
|
80
|
-
assert mod.__name__ == f"docx.oxml.{submodule}"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_submodule_attribute_access_raises() -> None:
|
|
84
|
-
"""The whole point of the shim — ``from docx.oxml.ns import qn``
|
|
85
|
-
raises :class:`OxmlNotAvailableError` with a message that names
|
|
86
|
-
the high-level surface, instead of crashing with the stdlib's
|
|
87
|
-
bare ``ModuleNotFoundError``."""
|
|
88
|
-
import docx.oxml.ns as ns
|
|
89
|
-
from docx.oxml import OxmlNotAvailableError
|
|
90
|
-
|
|
91
|
-
with pytest.raises(OxmlNotAvailableError) as exc_info:
|
|
92
|
-
_ = ns.qn
|
|
93
|
-
assert "docx.oxml.ns.qn" in str(exc_info.value)
|
|
94
|
-
# Same hint as the top-level package error.
|
|
95
|
-
assert "Document.add_paragraph" in str(exc_info.value)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def test_from_submodule_import_raises_typed_error() -> None:
|
|
99
|
-
"""``from docx.oxml.parser import OxmlElement`` must raise
|
|
100
|
-
:class:`OxmlNotAvailableError` even though the submodule import
|
|
101
|
-
itself succeeded — the attribute access happens during the
|
|
102
|
-
``from`` step and is the user-observable failure mode."""
|
|
103
|
-
from docx.oxml import OxmlNotAvailableError
|
|
104
|
-
|
|
105
|
-
import_stmt = "from docx.oxml.parser import OxmlElement"
|
|
106
|
-
with pytest.raises(OxmlNotAvailableError):
|
|
107
|
-
exec(import_stmt, {})
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_dunder_attribute_access_falls_through() -> None:
|
|
111
|
-
"""Dunder access (``__path__``, ``__loader__``, etc.) must NOT
|
|
112
|
-
raise :class:`OxmlNotAvailableError` — the import machinery probes
|
|
113
|
-
those during submodule resolution, and treating them as user
|
|
114
|
-
attribute access would break the whole import path. The stub
|
|
115
|
-
raises ``AttributeError`` for dunders so the import system can
|
|
116
|
-
fall back to its normal behavior."""
|
|
117
|
-
import docx.oxml.ns as ns
|
|
118
|
-
|
|
119
|
-
# Mimic what the import system does when it can't find an
|
|
120
|
-
# explicit attribute — should raise plain AttributeError, not
|
|
121
|
-
# OxmlNotAvailableError.
|
|
122
|
-
with pytest.raises(AttributeError):
|
|
123
|
-
_ = ns.__nonexistent_dunder__
|
|
File without changes
|