Skip to content

docx_plus.core.parts

Package part / relationship plumbing for separate OOXML parts. v0.2 capabilities (comments/, notes/) live in their own parts under /word/comments.xml, /word/footnotes.xml, and /word/endnotes.xml, each of which may be absent from a fresh document. get_or_create_part is the single entry point that returns the part and its parsed XML root, fabricating both if missing.

Internal XmlPart subclasses for footnote and endnote content types are registered with PartFactory.part_type_for at import time so existing documents round-trip with parsed .element rather than raw blobs.

Architecture walkthrough: ARCHITECTURE.md §7.5.

docx_plus.core.parts

Package part / relationship helpers for separate OOXML parts.

v0.1 capabilities (styles, fields, controls, protection) mutated only the main document part and settings.xml — both already present in every docx. v0.2 introduces capabilities backed by separate OOXML parts that may not exist in a fresh document:

  • /word/comments.xml (relationship :data:RT.COMMENTS)
  • /word/footnotes.xml (relationship :data:RT.FOOTNOTES)
  • /word/endnotes.xml (relationship :data:RT.ENDNOTES)

This module provides a single :func:get_or_create_part helper that the comments and notes packages use to look up an existing part or fabricate a fresh one with an empty default root.

python-docx already registers CommentsPart with PartFactory.part_type_for[CT.WML_COMMENTS] so existing comments parts deserialize with a parsed .element. It does not register footnote / endnote part classes, so this module installs minimal :class:~docx.opc.part.XmlPart subclasses for those content types at import time. Without that registration a round-tripped document with existing footnotes would surface them as raw blobs.

SPEC §2 lists the part / relationship plumbing as a v0.2 responsibility.

COMMENTS_SPEC module-attribute

COMMENTS_SPEC = PartSpec(
    partname="/word/comments.xml",
    content_type=WML_COMMENTS,
    relationship_type=COMMENTS,
    root_xml=_empty_root("comments"),
)

FOOTNOTES_SPEC module-attribute

FOOTNOTES_SPEC = PartSpec(
    partname="/word/footnotes.xml",
    content_type=WML_FOOTNOTES,
    relationship_type=FOOTNOTES,
    root_xml=_notes_root_with_separators("footnotes", "footnote", "separator"),
)

ENDNOTES_SPEC module-attribute

ENDNOTES_SPEC = PartSpec(
    partname="/word/endnotes.xml",
    content_type=WML_ENDNOTES,
    relationship_type=ENDNOTES,
    root_xml=_notes_root_with_separators("endnotes", "endnote", "separator"),
)

PartSpec dataclass

PartSpec(
    partname: str, content_type: str, relationship_type: str, root_xml: bytes
)

Identification for an OOXML part looked up or fabricated as a unit.

Parameters:

Name Type Description Default
partname str

Absolute package URI, e.g. "/word/comments.xml".

required
content_type str

Content-type URI from :class:docx.opc.constants.CONTENT_TYPE.

required
relationship_type str

Relationship-type URI from :class:docx.opc.constants.RELATIONSHIP_TYPE.

required
root_xml bytes

Complete XML bytes for a fresh, empty root element of this part (used only when creating a part that doesn't exist).

required

get_or_create_part

get_or_create_part(
    doc: Document, spec: PartSpec
) -> tuple[XmlPart, etree._Element]

Return (part, root_element) for the part identified by spec.

If the main document part already has a relationship of spec.relationship_type, returns the related part. Otherwise creates a fresh part with the empty default root from spec.root_xml and wires the relationship from the document part. Idempotent — a second call with the same spec returns the same part.

The part class is looked up in :attr:PartFactory.part_type_for so callers get the most specific class registered for the content type (CommentsPart for comments; the internal :class:_FootnotesPart / :class:_EndnotesPart for the note parts). Unknown content types fall back to :class:XmlPart.

Parameters:

Name Type Description Default
doc Document

The python-docx :class:~docx.document.Document whose part graph is being mutated.

required
spec PartSpec

Identifier for the target part. Use :data:COMMENTS_SPEC, :data:FOOTNOTES_SPEC, :data:ENDNOTES_SPEC, or build a :class:PartSpec for a custom part.

required

Returns:

Type Description
XmlPart

A tuple (part, root_element). root_element is

_Element

part.element — the XML root that caller modules mutate.

Source code in docx_plus/core/parts.py
def get_or_create_part(doc: Document, spec: PartSpec) -> tuple[XmlPart, etree._Element]:
    """Return ``(part, root_element)`` for the part identified by ``spec``.

    If the main document part already has a relationship of
    ``spec.relationship_type``, returns the related part. Otherwise
    creates a fresh part with the empty default root from ``spec.root_xml``
    and wires the relationship from the document part. Idempotent — a
    second call with the same spec returns the same part.

    The part class is looked up in :attr:`PartFactory.part_type_for` so
    callers get the most specific class registered for the content type
    (``CommentsPart`` for comments; the internal
    :class:`_FootnotesPart` / :class:`_EndnotesPart` for the note parts).
    Unknown content types fall back to :class:`XmlPart`.

    Args:
        doc: The python-docx :class:`~docx.document.Document` whose part
            graph is being mutated.
        spec: Identifier for the target part. Use :data:`COMMENTS_SPEC`,
            :data:`FOOTNOTES_SPEC`, :data:`ENDNOTES_SPEC`, or build a
            :class:`PartSpec` for a custom part.

    Returns:
        A tuple ``(part, root_element)``. ``root_element`` is
        ``part.element`` — the XML root that caller modules mutate.
    """
    document_part = doc.part
    try:
        part = cast("XmlPart", document_part.part_related_by(spec.relationship_type))
    except KeyError:
        package = document_part.package
        element = parse_xml(spec.root_xml)
        part_cls = cast(
            "type[XmlPart]",
            PartFactory.part_type_for.get(spec.content_type, XmlPart),
        )
        part = part_cls(PackURI(spec.partname), spec.content_type, element, package)
        document_part.relate_to(part, spec.relationship_type)
    return part, part.element