Schema-strict insertion¶
OOXML containers (CT_Style, CT_PPr, CT_RPr, CT_Settings, …) have
required child ordering. Inserting an element in the wrong position
produces a file Word will silently "repair" on open — which sometimes
works, sometimes doesn't, and is always a latent bug.
styles/modify.py enforces order via three canonical sequences:
_STYLE_CHILD_ORDER(modify.py:67-90) — the children of aw:styleelement_PPR_CHILD_ORDER(modify.py:92-129) — the children ofw:pPr_RPR_CHILD_ORDER(modify.py:131-...) — the children ofw:rPr
Every write goes through _ordered_insert(parent, new_child, order),
which finds the canonical position and inserts there, rather than
appending. The test_*_children_ordered_correctly family in
tests/test_styles_modify.py:277-340 verifies the invariant after
create_style. (Verification after modify_style is on the test-gap
list — see TEST_GAPS.md I2.)
All element construction goes through core/oxml.py's el() and
sub(). No bare lxml.etree.SubElement or python-docx OxmlElement
calls live in capability modules. This is enforced by the import-invariant
test — see invariant 2.
The settings.xml case¶
w:documentProtection, w:updateFields, and (v0.2) w:evenAndOddHeaders
all live deep in CT_Settings's child sequence (ECMA-376 17.15.1.78).
Every callsite applies the same
core/oxml.py:insert_before_first_anchor(parent, new_element, anchor_tags)
pattern, walking a tuple of later-siblings (w:defaultTabStop, w:compat,
w:rsids, etc.) and inserting before the first match. If no anchor is
present, the helper falls back to appending — the no-anchor case is
exercised by test_mark_fields_dirty_appends_when_no_anchor.
The helper lives in core/oxml.py (hoisted in v0.2 when
layout/settings.py became the third caller); the per-module anchor tuples
stay co-located with their callsites so the schema position is reviewed
alongside the new child.
core.ordered_insert (v0.5) is the stronger form: given the parent's full
child sequence it is idempotent, replacing any same-tag sibling rather than
adding a second. It was promoted out of styles/modify.py so
numbering could share it.