ProvenPin Install

python-docx report with styled tables and page breaks: working versions (tested 2026-10-16)

Generate a Word report with python-docx - a title, headings, a styled table and a page break - then reopen the .docx and verify structure, order and styling.

passed6 passed · 0 failed2026-10-16 21:57 UTC
Status
passed
Last passed
2026-10-16 21:57 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions2
python-docx@1.2.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38972656245

The code

Exactly the files that passed. Your agent can pull them with get_recipe("python-docx-table-report") (install), or see them on GitHub.

requirements.txt
python-docx==1.2.0
pytest==9.1.1
report.py
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.table import Table
from docx.text.paragraph import Paragraph

STYLE = "Light Grid Accent 1"


def build_report(path, title, columns, rows):
    doc = Document()
    doc.add_heading(title, level=0)
    doc.add_heading("Summary", level=1)
    doc.add_paragraph(f"{len(rows)} data rows")

    table = doc.add_table(rows=1, cols=len(columns))
    table.style = STYLE
    for cell, name in zip(table.rows[0].cells, columns):
        cell.text = name
        cell.paragraphs[0].runs[0].bold = True
    for row in rows:
        for cell, value in zip(table.add_row().cells, row):
            cell.text = str(value)

    doc.add_page_break()
    doc.add_heading("Appendix", level=1)
    doc.add_paragraph("Generated by ProvenPin.").alignment = WD_ALIGN_PARAGRAPH.CENTER
    doc.save(str(path))
    return doc


def iter_block_items(doc):
    # python-docx keeps paragraphs and tables in separate lists, so walk the XML
    # body to recover the real document order.
    for child in doc.element.body.iterchildren():
        if child.tag == qn("w:p"):
            yield Paragraph(child, doc)
        elif child.tag == qn("w:tbl"):
            yield Table(child, doc)


def headings(doc):
    return [(p.style.name, p.text) for p in doc.paragraphs if p.style.name.startswith("Heading")]


def has_page_break(paragraph):
    return any(
        br.get(qn("w:type")) == "page"
        for run in paragraph.runs
        for br in run._r.findall(qn("w:br"))
    )
tests/test_report.py
from docx import Document
from report import build_report, has_page_break, headings, iter_block_items

COLUMNS = ["name", "qty"]


def make(tmp_path):
    path = tmp_path / "report.docx"
    build_report(path, "Q3 Report", COLUMNS, [["apples", 3], ["pears", 5]])
    return Document(str(path))


def test_title_and_headings(tmp_path):
    doc = make(tmp_path)
    assert doc.paragraphs[0].style.name == "Title"
    assert doc.paragraphs[0].text == "Q3 Report"
    assert headings(doc) == [("Heading 1", "Summary"), ("Heading 1", "Appendix")]


def test_table_style_rows_and_cells(tmp_path):
    table = make(tmp_path).tables[0]
    assert table.style.name == "Light Grid Accent 1"
    assert len(table.rows) == 3
    assert [c.text for c in table.rows[0].cells] == COLUMNS
    assert table.cell(0, 0).paragraphs[0].runs[0].bold is True


def test_values_are_stringified(tmp_path):
    table = make(tmp_path).tables[0]
    assert table.cell(2, 1).text == "5"


def test_body_order_interleaves_tables_and_paragraphs(tmp_path):
    kinds = [type(b).__name__ for b in iter_block_items(make(tmp_path))]
    assert kinds == ["Paragraph", "Paragraph", "Paragraph", "Table", "Paragraph", "Paragraph", "Paragraph"]
    tables = [b for b in iter_block_items(make(tmp_path)) if type(b).__name__ == "Table"]
    assert len(tables) == 1


def test_page_break_is_an_empty_paragraph(tmp_path):
    doc = make(tmp_path)
    broken = [p for p in doc.paragraphs if has_page_break(p)]
    assert len(broken) == 1
    assert broken[0].text == ""


def test_unknown_table_style_raises_keyerror(tmp_path):
    doc = Document()
    table = doc.add_table(rows=1, cols=1)
    try:
        table.style = "No Such Style"
    except KeyError:
        pass
    else:
        raise AssertionError("expected KeyError")
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.