A Word-authored .docx that carries customXml/item*.xml and customXml/itemProps*.xml parts must still contain those parts after a python-docx load + save round-trip, even when the document body has no <w:sdt> element with a <w:dataBinding> pointing at them.
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/preserve-custom-xml |
|---|---|
| Format | docx |
| Category | roundtrip-fidelity |
| Spec | ecma-376-5-part-1 § 17.17 customXml |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
custom-xml-rel-preservedword/_rels/document.xml.rels | existThe document-level rel referring to the customXml item must still exist after round-trip. | //pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml'] | — | — |
custom-xml-content-type-preserved[Content_Types].xml | existThe Override ContentType for the customXml item-properties part must still be declared after round-trip. | //ct:Override[contains(@PartName, '/customXml/itemProps') and @ContentType='application/vnd.openxmlformats-officedocument.customXmlProperties+xml'] | — | — |
No render assertions declared.
scripts/gen_preserve_custom_xml.py
#!/usr/bin/env python3
"""Generate ``fixtures/docx/preserve-custom-xml.docx``.
The machine fixture is produced by round-tripping a Word-authored source
file through python-docx: open with ``docx.Document(<office>)`` then save
back with ``reproducible=True``. The source (.office.docx companion) is a
verbatim Word-authored file that carries ``customXml/item1.xml`` +
``customXml/itemProps1.xml`` but no ``<w:sdt>`` / ``<w:dataBinding>`` in
the body referencing them. The corresponding manifest asserts that the
document-level customXml relationship and the itemProps content-type
Override survive the round-trip.
Runs against current python-docx master will FAIL the manifest — that is
the point. python-docx currently drops unreferenced customXml in
``parts/document.py::DocumentPart._drop_unused_optional_parts``.
"""
from __future__ import annotations
from pathlib import Path
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
_OFFICE_SRC = _REPO_ROOT / "fixtures" / "docx" / "preserve-custom-xml.office.docx"
_OUT = _REPO_ROOT / "fixtures" / "docx" / "preserve-custom-xml.docx"
def main() -> int:
doc = Document(str(_OFFICE_SRC))
_OUT.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(_OUT), reproducible=True)
print(_OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/preserve-custom-xml",
"title": "Preserve customXml parts on load+save when no SDT data-binding is present",
"format": "docx",
"category": "roundtrip-fidelity",
"roles": [
"authoring"
],
"summary": "A Word-authored .docx that carries customXml/item*.xml and customXml/itemProps*.xml parts must still contain those parts after a python-docx load + save round-trip, even when the document body has no <w:sdt> element with a <w:dataBinding> pointing at them.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.17",
"element": "customXml",
"notes": "ECMA-376 Part 1 clause 17.17 defines a top-level 'Custom XML Data Storage Part' whose content type is application/xml and whose relationship type is .../officeDocument/2006/relationships/customXml. The intended use is arbitrary user-supplied XML attached to the package — most commonly driving content controls via <w:sdt>/<w:dataBinding/@w:xpath> bindings, but the spec does NOT require a binding to exist. Third-party authoring pipelines (template engines, DMS systems, SharePoint publishing) attach customXml parts that are read by external tooling, not by Word itself, and those parts must survive a library round-trip. python-docx currently drops the rel (and therefore the part) in parts/document.py::DocumentPart._drop_unused_optional_parts whenever its heuristic `any(sdt.find(w:dataBinding)) is False`. That heuristic gives a false positive on any document whose customXml is consumed outside Word. The .office.docx companion is a verbatim Word-authored file that has a customXml item with no dataBinding reference; the 'machine' fixture is that file re-saved by python-docx with reproducible=True."
},
"fixtures": {
"machine": "docx/preserve-custom-xml",
"office": "docx/preserve-custom-xml"
},
"generator": {
"python": "scripts/gen_preserve_custom_xml.py"
},
"assertions": [
{
"id": "custom-xml-rel-preserved",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"pr": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml']",
"must": "exist",
"description": "The document-level rel referring to the customXml item must still exist after round-trip."
},
{
"id": "custom-xml-content-type-preserved",
"part": "[Content_Types].xml",
"namespaces": {
"ct": "http://schemas.openxmlformats.org/package/2006/content-types"
},
"xpath": "//ct:Override[contains(@PartName, '/customXml/itemProps') and @ContentType='application/vnd.openxmlformats-officedocument.customXmlProperties+xml']",
"must": "exist",
"description": "The Override ContentType for the customXml item-properties part must still be declared after round-trip."
}
]
}
No rendered reference is available for this case.