Regression pin for a real python-docx lossy-read bug caught by Wave 5-A: loading paragraph-alignment.docx and immediately re-saving drops customXml, numbering, and stylesWithEffects parts along with their relationships, even though python-docx successfully loaded each into its object model. This manifest asserts the preserved-behaviour contract. It is RED until the bug is fixed; fixing it turns this manifest green and freezes the fix.
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/round-trip-orphan-parts-bug |
|---|---|
| Format | docx |
| Category | round-trip |
| Spec | ecma-376-5-part-3 § 9.3 Relationship |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
customxml-rel-preservedword/_rels/document.xml.rels | existThe customXml relationship (rId1 in the source) must survive the round-trip. Currently FAILS — python-docx drops it along with customXml/item1.xml, customXml/itemProps1.xml, customXml/_rels/item1.xml.rels. | //r:Relationships/r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml'] | — | — |
numbering-rel-preservedword/_rels/document.xml.rels | existThe numbering relationship (rId2) must survive. Currently FAILS — python-docx drops word/numbering.xml even though it loaded the NumberingPart into its object model. | //r:Relationships/r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering'] | — | — |
styleswitheffects-rel-preservedword/_rels/document.xml.rels | existThe Word 2010 stylesWithEffects relationship (rId4) must survive. Currently FAILS — python-docx drops it with word/stylesWithEffects.xml. | //r:Relationships/r:Relationship[@Type='http://schemas.microsoft.com/office/2007/relationships/stylesWithEffects'] | — | — |
numbering-part-present[Content_Types].xml | exist[Content_Types].xml must still declare word/numbering.xml — the companion to the numbering rel being preserved. | //ct:Override[@PartName='/word/numbering.xml'] | — | — |
customxml-itemprops-part-present[Content_Types].xml | exist[Content_Types].xml must still declare the customXml itemProps part. | //ct:Override[@PartName='/customXml/itemProps1.xml'] | — | — |
No render assertions declared.
scripts/gen_round_trip_orphan_parts_bug.py
#!/usr/bin/env python3
"""Generate round-trip-orphan-parts-bug twin.
KNOWN-FAILING regression fixture. Catches a real python-docx lossy-read
bug caught by Wave 5-A agent W5-A: when loading a document that carries
`customXml`, `numbering`, or `stylesWithEffects` relationships but the
document body does not syntactically reference them, python-docx loads
the related parts into its object model, then **silently drops them on
save**. The rels entries disappear along with the parts.
Concrete observed delta on ``fixtures/docx/paragraph-alignment.docx``:
Source rels on ``word/_rels/document.xml.rels``:
rId1 customXml -> ../customXml/item1.xml
rId2 numbering -> numbering.xml
rId3 styles -> styles.xml
rId4 stylesWithEffects -> stylesWithEffects.xml
rId5 settings -> settings.xml
rId6 webSettings -> webSettings.xml
rId7 fontTable -> fontTable.xml
rId8 theme -> theme/theme1.xml
Twin rels after `Document(p).save(out, reproducible=True)`:
rId3 styles -> styles.xml
rId5 settings -> settings.xml
rId6 webSettings -> webSettings.xml
rId7 fontTable -> fontTable.xml
rId8 theme -> theme/theme1.xml
Dropped: rId1 (customXml), rId2 (numbering), rId4 (stylesWithEffects).
Associated parts (``customXml/*``, ``word/numbering.xml``,
``word/stylesWithEffects.xml``, ``docProps/thumbnail.jpeg``) are also
missing from the twin zip.
For numbering.xml this is functionally dangerous: if any paragraph in
the body referenced a ``w:numId`` pointing to an ``abstractNum`` inside
numbering.xml, the twin would dangle that reference. (The specific
fixture here avoids that only because its body happens not to reference
any list.)
This manifest ships the twin anyway, and every assertion asserts the
bug is fixed — so the fixture starts life as a RED test. Once
python-docx learns to preserve orphan rels (either by walking them
unconditionally, or by only pruning rels whose target parts have
genuinely been removed from the package), this manifest turns green.
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
_FIX = _REPO_ROOT / "fixtures" / "docx"
def round_trip(source: Path, out: Path) -> None:
doc = Document(str(source))
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(out), reproducible=True)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--out", required=True, help="Twin output path (repo-relative).")
ns = parser.parse_args(argv)
source_path = _FIX / "paragraph-alignment.docx"
out_path = _REPO_ROOT / ns.out
if not source_path.exists():
print(f"Source fixture missing: {source_path}", file=sys.stderr)
return 1
round_trip(source_path, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/round-trip-orphan-parts-bug",
"title": "Round-trip: orphan part preservation (KNOWN FAILING — python-docx bug)",
"format": "docx",
"category": "round-trip",
"summary": "Regression pin for a real python-docx lossy-read bug caught by Wave 5-A: loading paragraph-alignment.docx and immediately re-saving drops customXml, numbering, and stylesWithEffects parts along with their relationships, even though python-docx successfully loaded each into its object model. This manifest asserts the preserved-behaviour contract. It is RED until the bug is fixed; fixing it turns this manifest green and freezes the fix.",
"spec": {
"source": "ecma-376-5-part-3",
"clause": "9.3",
"element": "Relationship",
"notes": "ECMA-376 Part 3 (OPC) does not require a relationship's target to be referenced from the source part's content stream. A rel is valid purely by virtue of being declared in a .rels file and having a present target part. Python-docx's save walker currently only emits rels whose target parts are reachable from the proxy object graph in a way that triggers re-serialization — orphaned rels silently disappear. For numbering.xml specifically this is dangerous: a document whose body DOES reference a numId via w:numPr would have a dangling pointer after round-trip. paragraph-alignment.docx was authored by Microsoft Word, which routinely ships numbering.xml and stylesWithEffects.xml proactively even for documents that don't yet use them; every such Word-authored docx round-tripped through python-docx loses them."
},
"fixtures": {
"machine": "docx/round-trip-orphan-parts-bug"
},
"round_trip": {
"library": "python-docx",
"source_fixture": "docx/paragraph-alignment",
"notes": "Mode B (generator-embedded). Source is the already-committed paragraph-alignment.docx (Word-authored Microsoft fixture). Twin is generated by a single python-docx load-save cycle. See gen_round_trip_orphan_parts_bug.py for the observed rels diff."
},
"generator": {
"python": "scripts/gen_round_trip_orphan_parts_bug.py",
"arg_template": "--out fixtures/docx/round-trip-orphan-parts-bug.docx"
},
"assertions": [
{
"id": "customxml-rel-preserved",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"r": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "//r:Relationships/r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml']",
"must": "exist",
"description": "The customXml relationship (rId1 in the source) must survive the round-trip. Currently FAILS — python-docx drops it along with customXml/item1.xml, customXml/itemProps1.xml, customXml/_rels/item1.xml.rels."
},
{
"id": "numbering-rel-preserved",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"r": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "//r:Relationships/r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering']",
"must": "exist",
"description": "The numbering relationship (rId2) must survive. Currently FAILS — python-docx drops word/numbering.xml even though it loaded the NumberingPart into its object model."
},
{
"id": "styleswitheffects-rel-preserved",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"r": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "//r:Relationships/r:Relationship[@Type='http://schemas.microsoft.com/office/2007/relationships/stylesWithEffects']",
"must": "exist",
"description": "The Word 2010 stylesWithEffects relationship (rId4) must survive. Currently FAILS — python-docx drops it with word/stylesWithEffects.xml."
},
{
"id": "numbering-part-present",
"part": "[Content_Types].xml",
"namespaces": {
"ct": "http://schemas.openxmlformats.org/package/2006/content-types"
},
"xpath": "//ct:Override[@PartName='/word/numbering.xml']",
"must": "exist",
"description": "[Content_Types].xml must still declare word/numbering.xml — the companion to the numbering rel being preserved."
},
{
"id": "customxml-itemprops-part-present",
"part": "[Content_Types].xml",
"namespaces": {
"ct": "http://schemas.openxmlformats.org/package/2006/content-types"
},
"xpath": "//ct:Override[@PartName='/customXml/itemProps1.xml']",
"must": "exist",
"description": "[Content_Types].xml must still declare the customXml itemProps part."
}
]
}
No rendered reference is available for this case.