Skip to content

docx_plus.styles.inspect

The cascade resolver. Walks the six OOXML formatting layers and returns a fully-resolved ResolvedFormatting plus optional per-field provenance.

See ARCHITECTURE.md §2 for the algorithm walkthrough and the toggle semantics.

docx_plus.styles.inspect

Cascade resolver: resolve_effective_formatting.

Walks the six layers of OOXML formatting precedence (SPEC §4) and returns a fully-resolved :class:ResolvedFormatting describing what a paragraph, run, or cell would render with right now. Later layers override earlier ones, except toggle properties (bold, italic, etc.) which XOR through the chain per ECMA-376 17.7.3.

Provenance tracking is plumbed through the same walk gated by the include_provenance flag; with the flag off, the resolver's value output is identical (verified by test_provenance_does_not_change_values).

ResolvedFormatting dataclass

ResolvedFormatting(
    style_id: str | None = None,
    style_name: str | None = None,
    alignment: str | None = None,
    indent_left: int | None = None,
    indent_right: int | None = None,
    indent_first_line: int | None = None,
    spacing_before: int | None = None,
    spacing_after: int | None = None,
    line_spacing: float | None = None,
    line_spacing_rule: str | None = None,
    keep_with_next: bool | None = None,
    keep_lines: bool | None = None,
    page_break_before: bool | None = None,
    outline_level: int | None = None,
    font_name: str | None = None,
    font_size: float | None = None,
    bold: bool | None = None,
    italic: bool | None = None,
    cs_bold: bool | None = None,
    cs_italic: bool | None = None,
    underline: str | None = None,
    strike: bool | None = None,
    double_strike: bool | None = None,
    color_rgb: str | None = None,
    highlight: str | None = None,
    caps: bool | None = None,
    small_caps: bool | None = None,
    vanish: bool | None = None,
    emboss: bool | None = None,
    imprint: bool | None = None,
    outline: bool | None = None,
    shadow: bool | None = None,
    vert_align: str | None = None,
    num_id: int | None = None,
    num_level: int | None = None,
    partial: bool = False,
    provenance: dict[str, FormattingSource] | None = None,
)

The effective formatting for a paragraph, run, or table cell.

Every field is None until some layer of the cascade sets it. Toggle properties carry their XOR-resolved boolean. SPEC §4 specifies the fields.

All twelve ECMA-376 17.7.3 toggle properties are surfaced: the six base toggles (bold, italic, caps, small_caps, strike, vanish) and the six complex-script / decorative variants (cs_bold, cs_italic, emboss, imprint, outline, shadow). All XOR through the cascade with the same semantics; an explicit w:val="false" resets parity to false.

TableContext dataclass

TableContext(
    is_first_row: bool = False,
    is_last_row: bool = False,
    is_first_col: bool = False,
    is_last_col: bool = False,
    is_band_row: bool = False,
    is_band_col: bool = False,
    is_band2_row: bool = False,
    is_band2_col: bool = False,
)

A cell's position within its table — for conditional table-style formatting.

ECMA-376 17.7.6.5 lets a <w:style w:type="table"> carry conditional formatting branches (<w:tblStylePr w:type="firstRow"/>, "lastRow", "firstCol", "lastCol", "band1Horz", "band1Vert", "band2Horz", "band2Vert", "nwCell" / "neCell" / "swCell" / "seCell"). To pick the right branches the cascade resolver needs to know where in the table the target lives.

Construct manually for an out-of-band query, or pass a _Cell to :func:resolve_effective_formatting to derive the context automatically from the cell's parent row / table.

Band size: by default rows alternate band1 / band2 every row. When the table instance's <w:tblPr> carries a <w:tblStyleRowBandSize w:val="N"/> (resp. <w:tblStyleColBandSize>), bands span N rows / columns each. Note that v0.2 does not yet walk the table style chain looking for these attributes — only the table instance's own tblPr is consulted. This is sufficient for tables where the application or user explicitly set the band size, but misses style-defined band sizes (deferred to v0.3+).

Scope: this context selects which <w:tblStylePr> branches apply, but only their run / paragraph properties are resolved. Cell-, row-, and table-level properties (<w:tcPr> / <w:trPr> / <w:tblPr>) from a table style are not surfaced — see the :func:resolve_effective_formatting note.

Auto-derivation limitation: when a row wraps its cells in a <w:sdt> (a content control around table cells), the derived column index cannot be computed and an empty (all-False) :class:TableContext is returned. Pass an explicit context in that case. Nested tables resolve against the inner cell's position.

Attributes:

Name Type Description
is_first_row bool

Cell is in the first <w:tr> of its table.

is_last_row bool

Cell is in the last <w:tr>.

is_first_col bool

Cell is the first <w:tc> of its row.

is_last_col bool

Cell is the last <w:tc> of its row.

is_band_row bool

Cell is in a "band1" horizontal stripe (first band).

is_band_col bool

Cell is in a "band1" vertical stripe (first band).

is_band2_row bool

Cell is in a "band2" horizontal stripe (second band — the complement of band1 at default band-size=1).

is_band2_col bool

Cell is in a "band2" vertical stripe.

FormattingSource dataclass

FormattingSource(
    layer: Layer,
    style_id: str | None = None,
    is_toggle_resolved: bool = False,
    chain_depth: int | None = None,
)

Identifies the cascade layer that contributed a resolved property.

layer is the cascade layer the value came from. For style layers, style_id names the specific style (the lowest one in the basedOn chain that set the value); chain_depth records how many basedOn hops away that style was from the target. is_toggle_resolved is True when the value is the XOR result across multiple layers rather than a direct set.

StyleCascadeError

Bases: DocxPlusError

Raised when the basedOn chain cycles or exceeds Word's depth limit.

MissingPartError

Bases: DocxPlusError

Raised when a referenced document part (e.g. numbering.xml) is absent.

resolve_effective_formatting

resolve_effective_formatting(
    target: Paragraph | Run | _Cell,
    *,
    include_provenance: bool = False,
    table_context: TableContext | None = None,
) -> ResolvedFormatting

Resolve the effective formatting for target.

Walks the six cascade layers in precedence order, returning a fully resolved :class:ResolvedFormatting. Toggle properties XOR through the chain per ECMA-376 17.7.3. Theme colors are resolved against the document's theme part; if the theme is missing or malformed, the result's partial flag is set and unresolved theme names are returned in place of hex values.

When target is in a table cell, table-style conditional formatting (<w:tblStylePr> branches: firstRow, lastRow, firstCol, lastCol, band1Horz, band1Vert, the four corners, and wholeTable) is applied on top of the base table style in ECMA-376 17.7.6.5 precedence order.

Note

Only run- and paragraph-level properties are resolved (the <w:rPr> / <w:pPr> carried by a style's base and its <w:tblStylePr> branches). Cell-, row-, and table-level properties (<w:tcPr> cell shading and margins, <w:trPr> row heights, <w:tblPr> table defaults) declared by a table style are not surfaced on :class:ResolvedFormatting — that belongs to a separate cell-formatting resolver deferred to v0.3+.

Parameters:

Name Type Description Default
target Paragraph | Run | _Cell

A python-docx :class:~docx.text.paragraph.Paragraph, :class:~docx.text.run.Run, or :class:~docx.table._Cell.

required
include_provenance bool

If True, populate .provenance with the cascade layer that set each field. Default False.

False
table_context TableContext | None

Optional override for the cell's position within its table. When None (default), the resolver derives it from the target's parent <w:tr> / <w:tbl> chain; pass an explicit :class:TableContext to query a hypothetical position (e.g. "what would the formatting be if this cell were in the first row?").

None

Returns:

Name Type Description
A ResolvedFormatting

class:ResolvedFormatting snapshot.

Raises:

Type Description
StyleCascadeError

If the basedOn chain has a cycle or exceeds Word's depth limit of 11.

MissingPartError

If the target's paragraph references a numbering id that exists but the numbering.xml part itself is absent.

Example

from docx import Document from docx_plus.styles.inspect import resolve_effective_formatting doc = Document() p = doc.add_paragraph("Hello") resolved = resolve_effective_formatting(p) resolved.font_size # e.g. 11.0 from docDefaults 11.0

Source code in docx_plus/styles/inspect.py
def resolve_effective_formatting(
    target: Paragraph | Run | _Cell,
    *,
    include_provenance: bool = False,
    table_context: TableContext | None = None,
) -> ResolvedFormatting:
    """Resolve the effective formatting for ``target``.

    Walks the six cascade layers in precedence order, returning a fully
    resolved :class:`ResolvedFormatting`. Toggle properties XOR through the
    chain per ECMA-376 17.7.3. Theme colors are resolved against the
    document's theme part; if the theme is missing or malformed, the result's
    ``partial`` flag is set and unresolved theme names are returned in place
    of hex values.

    When ``target`` is in a table cell, table-style **conditional
    formatting** (``<w:tblStylePr>`` branches: ``firstRow``, ``lastRow``,
    ``firstCol``, ``lastCol``, ``band1Horz``, ``band1Vert``, the four
    corners, and ``wholeTable``) is applied on top of the base table
    style in ECMA-376 17.7.6.5 precedence order.

    Note:
        Only **run- and paragraph-level** properties are resolved (the
        ``<w:rPr>`` / ``<w:pPr>`` carried by a style's base and its
        ``<w:tblStylePr>`` branches). Cell-, row-, and table-level
        properties (``<w:tcPr>`` cell shading and margins, ``<w:trPr>``
        row heights, ``<w:tblPr>`` table defaults) declared by a table
        style are **not** surfaced on :class:`ResolvedFormatting` — that
        belongs to a separate cell-formatting resolver deferred to v0.3+.

    Args:
        target: A python-docx :class:`~docx.text.paragraph.Paragraph`,
            :class:`~docx.text.run.Run`, or :class:`~docx.table._Cell`.
        include_provenance: If True, populate ``.provenance`` with the cascade
            layer that set each field. Default False.
        table_context: Optional override for the cell's position within
            its table. When ``None`` (default), the resolver derives it
            from the target's parent ``<w:tr>`` / ``<w:tbl>`` chain;
            pass an explicit :class:`TableContext` to query a hypothetical
            position (e.g. "what would the formatting be if this cell
            were in the first row?").

    Returns:
        A :class:`ResolvedFormatting` snapshot.

    Raises:
        StyleCascadeError: If the basedOn chain has a cycle or exceeds Word's
            depth limit of 11.
        MissingPartError: If the target's paragraph references a numbering id
            that exists but the ``numbering.xml`` part itself is absent.

    Example:
        >>> from docx import Document
        >>> from docx_plus.styles.inspect import resolve_effective_formatting
        >>> doc = Document()
        >>> p = doc.add_paragraph("Hello")
        >>> resolved = resolve_effective_formatting(p)
        >>> resolved.font_size  # e.g. 11.0 from docDefaults
        11.0
    """
    target_kind, target_el = _classify_target(target)
    doc = _document_of(target)
    styles_root = doc.styles.element
    theme = load_theme(doc)

    # ``partial`` is set lazily — only when a theme reference actually fails
    # to resolve (inside _resolve_color / _resolve_font_theme). A missing
    # theme part is not, on its own, an incomplete resolution: a document
    # with no theme refs resolves fully even without a theme (SPEC §4).
    acc = _Accumulator(theme=theme, want_provenance=include_provenance)

    # _classify_target returns the underlying element alongside the kind, so
    # the union-attr access happens once where isinstance has already narrowed
    # the type — no per-branch type: ignore needed here.
    if target_kind == "paragraph":
        ctx = table_context or _derive_table_context_from_element(target_el)
        _apply_paragraph_cascade(acc, doc, styles_root, target_el, table_context=ctx)
    elif target_kind == "run":
        paragraph_element = _enclosing_paragraph(target_el)
        ctx = table_context or _derive_table_context_from_element(paragraph_element)
        _apply_paragraph_cascade(
            acc,
            doc,
            styles_root,
            paragraph_element,
            run_element=target_el,
            table_context=ctx,
        )
    else:  # cell
        ctx = table_context or _derive_table_context_from_element(target_el)
        _apply_cell_cascade(acc, styles_root, target_el, table_context=ctx)

    return acc.freeze()