Conversation
Python tables stopped at the python-docx basics, so a document that needs borders, shading, cell or table margins, grid widths, a row height, a row kept on one page or a repeating header row still needed an lxml pass, although each of those has a checked setter in the rdocx facade. Table gains set_borders, set_border, border, cell_margins, set_cell_margins, grid_widths and set_column_width. Row gains height and height_rule, with a WD_ROW_HEIGHT_RULE enum as in python-docx, plus cant_split and is_header. Cell gains shading, border, set_border, margins and set_margins. Each call goes to the checked native setter, so a rejected value leaves the document unchanged, and none of them moves content, so live handles stay valid. The native reader reports no height for a w:trHeight with an auto rule or without a value, so Row.height reads None there, unlike python-docx, and assigning a height writes a minimum. The stub and HLD 10 say so. GitHub issue tensorbee#168.
Python could only append a table, and it had no way to merge cells, so a table placed between two paragraphs or a merged header still needed lxml. The native facade has insert_table and the checked table-level merge operations, while the Cell span setter it also has is unchecked and leaves an invalid grid behind. Document.insert_table(index, rows, cols) rejects an index past the end before mutation and returns a handle to the new table. Table handles count tables inside block content controls too, so the handle is found from the inserted body position rather than assumed. Table.set_cell_grid_span and Table.set_cell_vertical_merge call the checked operations, which validate the whole table first. A span that absorbs or restores cells advances the revision, because later cell indexes move, and a vertical merge keeps handles valid. Cell.grid_span and Cell.vertical_merge read the result. A Python test runs the table acceptance workflow of the issue, with the formatting setters, through the binding, and an rdocx integration test makes the same native calls. Both pin the same body XML, so CI checks that the binding writes what the native calls write. GitHub issue tensorbee#168.
Python sections were frozen snapshots with no mutation entry point, so assigning margin_top failed with "attribute is not writable", while the native facade has checked setters for every section value and staged insert_section and remove_section. Document.update_section(index, **values) takes the snapshot's own field names as keywords and returns the new snapshot, so Section stays a frozen value. The native setters write page size, margins, columns and header and footer distances together, so a value given alone keeps its partners as the section has them, and a partner the section never set raises ValueError instead of being invented. Column partners come from the equal-width view the snapshot reports, so a column count or spacing given alone raises rather than rewriting unequal-width tracks. Every name and partner is checked before any change, and a value a native setter rejects restores the section, so a failed call leaves the document unchanged. Section edits move no content and keep handles valid. insert_section and remove_section advance the revision because they change the body. rdocx-py now depends on rdocx-oxml, as rdocx-cli does, to name the orientation and break types the facade setters take. A section made by insert_section has no w:pgMar, so setting its margins wrote an element without the gutter, header and footer attributes that CT_PageMar requires. The native Section margin, gutter and distance setters now fill every w:pgMar value the section lacks with the default that layout already assumes for it, so the element is complete and the pages render as before. A Python test and an rdocx integration test make the same section edits through the binding and natively, and both pin the same body XML. GitHub issue tensorbee#168.
The native section setters that now write a complete w:pgMar and the integration tests that pin the body the Python table and section workflows write grow the rdocx package, so its README archive row and its ARCHIVE_MEASUREMENTS entry are re-measured. GitHub issue tensorbee#168.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Tablegainsset_borders,set_border,border,cell_margins,set_cell_margins,grid_widths(read and assign) andset_column_width.Rowgainsheightandheight_rulewith a newWD_ROW_HEIGHT_RULEenum as inpython-docx, plus the tri-state
cant_splitandis_header.Cellgainsshading,border,set_border,marginsandset_margins.Document.insert_table(index, rows, cols)and cell merges through thechecked table operations,
Table.set_cell_grid_span(row, col, span)andTable.set_cell_vertical_merge(row, col, merge), withCell.grid_spanand
Cell.vertical_mergeto read the result.Document.update_section(index, **values),insert_section(index)andremove_section(index).Sectionstays a frozen snapshot, andupdate_sectionreturns the new one. rdocx-py now depends onrdocx-oxml, as rdocx-cli already does, to name the orientation and breaktypes the section setters take.
Sectionmargin, gutter and header and footer distancesetters write a complete
w:pgMar. A section made byinsert_sectionhasnone, and setting its margins wrote an element without the
gutter,headerandfooterattributes thatCT_PageMarrequires.Part of #168. This covers checklist items A (tables) and C (sections). Item B (styles and lists) is not
part of this PR and is left for a follow-up, see the notes. Items D
(bookmarks and fields), E (section stories), F (counted replacement), H
(rendering) and the native-feature items I to O remain, as planned in their
own PRs.
Why
Python tables stopped at the python-docx basics and Python sections were read
only, so the production chain of #168 kept an lxml pass for borders, shading,
cell and table margins, grid widths, row height, rows kept on one page,
repeating header rows, a table placed between two paragraphs, merged cells,
and page margins or orientation. Every one of those already had a checked
entry point in the rdocx facade.
doc.sections[0].margin_top = 720failedwith "attribute 'margin_top' of 'builtins.Section' objects is not writable".
Notes
rdocx-pycrate. The one native change is thew:pgMarcompletion inrdocx:Section::set_margins,set_gutterandset_header_footer_distancefill every otherw:pgMarvalue the sectionlacks with the default that layout already assumes for it (one inch margins,
half inch header and footer distances, no gutter), so the element carries
all seven required attributes and the pages render as before. A value the
section already has is kept. After such a call the section reports those
filled values, for example
header_distanceof half an inch afterupdate_sectionsets the margins of an inserted section. The legacyfinal-section
Documentsetters and the serializer are unchanged, so aforeign part with an incomplete
w:pgMarstill round-trips as it was. Thehash harness has no delta, because the samples use the legacy setters. The
rdocx crates.io archive row is re-recorded in the last commit, and the lock
file change is the new
rdocx-oxmldependency edge ofrdocx-py.Row.heightplusRow.height_rule, as python-docx does, withWD_ROW_HEIGHT_RULE.AT_LEAST(1) andEXACTLY(2).AUTOis left outbecause the native row height has no auto form, the same way the table
enums leave out values the facade cannot write. Assigning a height keeps
the rule of an exact height and otherwise writes a minimum. Assigning a
rule needs a height to apply to and raises
ValueErrorbefore oneexists. The native reader reports no height for a
w:trHeightwith anautorule or without a value, so both properties readNonethere,where python-docx reads the value and
AUTO, and assigning a heightthen writes a minimum. The stub and HLD 10 say so. Reading those forms
would need a native accessor for the raw rule.
cant_splitandis_headerare optional booleans, andNoneremoves the direct value.set_cell_grid_span_checkedandset_cell_vertical_mergeonTable. The uncheckedCell::set_grid_spanthe issue cites is not bound, because it writes
w:gridSpanwithoutremoving the absorbed cells. A python-docx shaped
Cell.merge(other)canbe composed from these two later.
Document.update_section(index, **values), whose keywords are theSectionsnapshot's own field names.A value given alone keeps its partners (page size, the four margins,
column count and spacing, header and footer distance) from the section's
current explicit values. Column partners come from the equal-width view
the snapshot reports, so on a section laid out in unequal-width tracks
(
w:equalWidth="0"orw:colchildren)column_countorcolumn_spacingalone raisesValueErrorrather than rewriting thetracks, and both together replace them with equal-width columns.
recommended answer:
margin_topalone on asection inserted with
insert_section, which has now:pgMar) raisesValueErrornaming the missing field rather than being filled with thelayout default of one inch. Recommended: keep, so the binding never
invents a value the caller edits. The native
w:pgMarcompletion aboveis separate: it fills only attributes the schema requires, with the
values layout already used.
update_sectionis atomic. Names and partners are checked before anychange, and when a native setter rejects a later value after an earlier
one applied, the section properties are restored from a clone. The
native
Sectionsetters are individually checked only, so this is theone place where the binding adds staging of its own. Recommended: keep,
or move the same staging into a native
Section::updatelater.dotDash,insideH), as the existing string values of the binding do(
nextPage,darkBlue). Border size is an integer in eighths of apoint, the native unit, with the native 0 to 96 validation. Recommended:
keep both.
Row.height,Row.height_ruleandCell.shadingcannot be assignedNone, because the facade has no setter that removes them. Recommended:add native removal only when a caller needs it.
because later cell indexes move. A vertical merge and every formatting
setter keep live handles valid, like
Table.widthtoday. Recommended:keep.
native setter does. A section without a page size (for example one made
by
insert_section) needspage_widthandpage_heightin the samecall to lay out landscape. Page size is applied before orientation.
Section.margin_topstays read only by design, so the last lines ofprobe_bindings.pyfrom Production readiness for editing real docx and pptx files: an acceptance contract, two realistic fixtures and two matrices #158 still report the attribute as not writable.update_sectionis the entry point.three focused commits and an archive re-record, and B carries the API shape decisions with the
longest life on a published wheel. Proposed shape for the follow-up:
add_style(style_id, name, *, style_type="paragraph", based_on=None, next_style=None, linked_style=None, priority=None, quick_format=None, font_name=None, font_size=None, bold=None, italic=None, color=None, alignment=None, space_before=None, space_after=None, line_spacing=None, left_indent=None, first_line_indent=None, keep_with_next=None, keep_together=None, page_break_before=None) -> Stylewith the value typesof
FontandParagraphFormat,set_stylewith the same keywords whereNoneleaves a value and aclear=tuple of names removes one,remove_style,set_default_style, a frozenNumberingLevel,add_numbering_definition,add_numbering_instance,link_style_to_numberingandunlink_style_from_numbering. Two facts forthat PR:
ListNumberFormathas no public string conversion, so eitherthe facade gains one or the binding maps the
ST_NumberFormatnamesitself, and Python cannot see list marker text in
Document.layout(), so"rendered numbered" is best checked by a Rust test walking the layout
text.
and Align split_run, bookmarks and story comments on the direct body index #179 add. The Python tests sit next to the related table and section
tests, and the native tests next to the related rdocx table and section
tests.
Tests
crates/rdocx-py/tests/test_formatting_tables.py, placed aftertest_table_rows_are_cloned_with_their_formatting_and_removed:test_table_borders_margins_and_grid_widths_round_tripchecks thew:tblBorders,w:tblCellMarandw:tblGridXML and every getter aftera reopen, including the table width and covering cell widths kept in
step.
test_row_height_split_and_header_round_tripchecksw:trHeightwithw:hRule="exact",w:cantSplit,w:tblHeader, an explicit false andclearing with
None.test_cell_shading_borders_and_margins_round_tripchecksw:shd,w:tcBordersand the margins after a reopen.test_invalid_table_formatting_changes_nothing_and_keeps_handles_liveruns fourteen rejected calls (unknown style, edge and rule, width over 96
eighths, bad color, negative margins, wrong grid length, zero grid
column, out-of-range column, negative column width, negative height,
rule without height) and checks the package bytes are unchanged and the
held handles still work.
test_insert_table_at_a_body_index_returns_the_new_tableinsertsbetween a block content control holding a table and a paragraph, checks
the body order and that the returned handle edits the new table rather
than the one inside the control, and that an index past the end raises
before mutation.
test_cells_merge_through_the_checked_table_operationschecksw:gridSpan,w:vMergerestart and continue, the reduced cell count,stale handles after a span but not after a vertical merge, the rejected
merges (nonempty cell, no matching cell above, past the grid, unknown
name, bad row) with unchanged bytes, and undoing both merges.
test_table_acceptance_workflow_writes_the_native_bodyis the Aacceptance of the issue: a table inserted at a body index, two cells
merged horizontally and vertically, borders, shading, margins, widths,
row heights, a row kept on one page and a repeating header row, plus
one table edge, table cell margins, the grid and a cell border. It pins
the resulting
w:body.crates/rdocx-py/tests/test_core.py, placed aftertest_word_structure_snapshots_preserve_order_ownership_and_types:test_update_section_margins_and_orientation_reach_the_layoutis the Cacceptance: margins and orientation of the section changed from Python,
then
layout_page(0)is 792 by 612 points and the first body fragmentstarts at the new left and top margins (144 and 36 points).
test_update_section_keeps_unnamed_partners_and_rejects_atomicallychecks partner filling for page size, distances and columns, and six
rejected calls, two of which fail in a native setter after an earlier
value applied, with unchanged bytes.
test_update_section_never_rewrites_unequal_width_columnsreads asection with two explicit
w:coltracks as no column count or spacing,checks that either value alone raises with unchanged bytes, and that
both together replace the tracks.
test_insert_and_remove_section_restructure_the_bodyinserts a section,makes it landscape and checks that page 2 of the layout is landscape,
then removes it, with stale handles, index errors and the sole section
refusal.
test_section_edits_write_the_native_bodyedits every section keywordacross an original and an inserted section, with partner filling, and
pins the resulting
w:body.crates/rdocx/tests/integration_test.rs:table_acceptance_workflow_writes_the_body_the_python_binding_pinsandsection_edits_write_the_body_the_python_binding_pinsmake the nativecalls the two Python workflows bind and pin the same
w:bodystrings,so CI, which runs pytest and the rdocx tests but not the rdocx-py Rust
tests, checks that the Python calls and the native calls write the same
body. Both sides compare the body on one line with the indentation
removed.
section_page_margin_setters_write_every_required_attributesets thegutter, the margins and the distances of three inserted sections to the
values layout assumes, checks that the deterministic PDF is byte
identical to the untouched document and that all four
w:pgMarelements carry the seven attributes, then that later setters keep the
values the section already has.
typing_smoke.pyexercises every new entry point, and the stub, mypy andstubtest agree.
origin/maindoes not have. Without thenative
w:pgMarchange,section_page_margin_setters_write_every_required_attributeandsection_edits_write_the_body_the_python_binding_pinsfail, because aninserted section gains only the attributes its setter names.
cargo fmt --all --check,cargo clippy -p rdocx -p rdocx-py --all-targets --all-features -- -D warnings,python3 scripts/hash_harness.py --check(49 entries match),python3 scripts/prose_check.py,python3 scripts/sync_agent_skills.py --checkand
python3 scripts/readme_doctests.py(with the rdocx archive rowre-recorded) are clean.
cargo test -p rdocx --all-features --no-fail-fastpasses apart from the environment-only failures below(lib 469 passed, integration 317 passed, regression 561 passed, doctests
2 passed). The Python gate on a
maturin developbuild in a Python 3.12venv with python-docx 1.2.0:
pytest crates/rdocx-py/testsgives 77passed and 2 failed, and
mypy --strictontyping_smoke.pyand thepackage and
mypy.stubtest rdocxare clean.test_rendering_threads.pytestsassert the pinned pdfinfo 26.01.0 against the local 26.09.0. In
cargo test -p rdocx, the lib testslarge_word_and_presentation_pdfs_preserve_logical_reading_orderandword_and_powerpoint_chart_pixels_are_identicaland the integration testsodt_reader_matches_pinned_libreoffice_structure,public_authored_theme_and_fonts_match_pinned_word_resolution,sanitized_public_authoring_fixture_passes_every_conformance_stage,section_page_semantics_match_pinned_libreoffice_renderandevery_conditional_table_region_matches_wordassert pinned toolversions (LibreOffice 26.2.5.2, pdftotext and the Poppler rasterizer)
against the local ones, or, for the conformance harness, fail in its
offline external consumer build. None of them uses the changed section
setters.