ProvenPin Install

reportlab Platypus SimpleDocTemplate with Table and Paragraph styles: working versions (tested 2026-10-16)

Render a PDF report from a Platypus story: SimpleDocTemplate, custom ParagraphStyle headings, and a styled Table with zebra rows and a repeating header.

passed6 passed · 0 failed2026-10-16 09:57 UTC
Status
passed
Last passed
2026-10-16 09:57 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions2
reportlab@5.0.1pytest@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 38883899161

The code

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

requirements.txt
reportlab==5.0.1
pytest==9.1.1
report.py
from io import BytesIO
from xml.sax.saxutils import escape

from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle

HEADER_BG = colors.HexColor("#1F4E79")
ZEBRA = colors.HexColor("#F2F2F2")


def styles():
    base = getSampleStyleSheet()
    return {
        "title": ParagraphStyle("ReportTitle", parent=base["Title"], fontSize=16, textColor=HEADER_BG),
        "head": ParagraphStyle("ReportHead", parent=base["BodyText"], fontName="Helvetica-Bold", fontSize=9, textColor=colors.white),
        "cell": ParagraphStyle("ReportCell", parent=base["BodyText"], fontSize=9, leading=11),
    }


def paragraph(text, style):
    return Paragraph(escape(str(text)), style)


def table_style():
    return TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), HEADER_BG),
        ("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, ZEBRA]),
    ])


def items_table(items, st, col_widths=(8 * cm, 3 * cm)):
    if not items:
        raise ValueError("items must not be empty")
    data = [[paragraph("Item", st["head"]), paragraph("Qty", st["head"])]]
    data += [[paragraph(name, st["cell"]), paragraph(qty, st["cell"])] for name, qty in items]
    table = Table(data, colWidths=col_widths, repeatRows=1)
    table.setStyle(table_style())
    return table


def build_pdf(items, buf=None):
    buf = buf or BytesIO()
    st = styles()
    story = [paragraph("Quarterly report", st["title"]), Spacer(1, 0.4 * cm), items_table(items, st)]
    SimpleDocTemplate(buf, pagesize=A4, title="Quarterly report").build(story)
    return buf.getvalue()
tests/test_report.py
from io import BytesIO

import pytest
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.platypus import Paragraph, SimpleDocTemplate, Table, TableStyle

from report import build_pdf, items_table, paragraph, styles, table_style


def test_build_pdf_emits_a_valid_document():
    pdf = build_pdf([("Widget", 3), ("Gadget", 7)])
    assert pdf.startswith(b"%PDF-")
    assert b"%%EOF" in pdf[-64:]


def test_paragraph_escapes_markup_so_ampersands_survive():
    st = styles()
    assert paragraph("AT&T", st["cell"]).getPlainText() == "AT&T"
    assert paragraph("<b>bold</b>", st["cell"]).getPlainText() == "<b>bold</b>"
    raw = Paragraph("AT&T", st["cell"]).getPlainText()
    assert raw == "AT&T;"


def test_empty_items_are_rejected():
    with pytest.raises(ValueError):
        items_table([], styles())


def test_paragraph_cells_wrap_while_plain_strings_overflow():
    st = styles()
    long_text = "lorem ipsum " * 20
    wrapped = Table([[Paragraph(long_text, st["cell"])]], colWidths=[4 * cm])
    wrapped.setStyle(TableStyle([("GRID", (0, 0), (-1, -1), 0.5, colors.grey)]))
    plain = Table([[long_text]], colWidths=[4 * cm])
    assert wrapped.wrap(4 * cm, 100 * cm)[1] > plain.wrap(4 * cm, 100 * cm)[1]


def test_large_table_splits_and_repeats_the_header():
    table = items_table([(f"Item {i}", i) for i in range(200)], styles())
    assert table.repeatRows == 1
    buf = BytesIO()
    doc = SimpleDocTemplate(buf)
    doc.build([table])
    assert doc.page > 1


def test_table_style_keeps_the_header_out_of_the_zebra_rows():
    commands = table_style().getCommands()
    zebra = next(c for c in commands if c[0] == "ROWBACKGROUNDS")
    header = next(c for c in commands if c[0] == "BACKGROUND" and c[2] == (-1, 0))
    assert zebra[1] == (0, 1)
    assert header[3] == colors.HexColor("#1F4E79")
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.