Genro Builders for Dummies

Domain-specific grammars for structured output — define once, render anywhere

The Problem

You need to generate structured output — HTML pages, Markdown documents, XML files, configuration files, PDF reports. You start with string concatenation. Then templates. Then a DSL. Each project reinvents the same pattern: define a structure, validate it, produce output.

Worse: what if the same data needs to become HTML and PDF and a live widget tree? Now you have three generators that don't share anything.

Genro Builders separates structure from output. You define a grammar (what tags exist, what can contain what), describe your content as a tree, and then render it to any format. One description, many outputs. And it's reactive — change the data, the output updates automatically.

GRAMMAR @element • @abstract • @component SOURCE Your content tree + ^pointers BUILT Components expanded, pointers resolved HTML Markdown PDF Live Widgets build() render() compile()

When does this actually matter?

Same data, many formats

A report that needs to be HTML for the web, PDF for printing, and Markdown for documentation — from the same source tree.

Validated structure

The grammar catches mistakes at creation time: wrong children, missing required elements, cardinality violations. No broken output.

Reactive output

Change a data value after build — the output updates automatically. No manual re-rendering.

Reusable components

@component defines composite structures that expand lazily. A login_form always generates the right inputs.

Domain-specific API

Your code reads like the domain: menu.first_courses().pasta(name="Lasagne") — not generic tree manipulation.

Manager = document

Use a BuilderManager, define store() and main() — your class is the document. Reusable, testable, composable.

Your First Builder in 30 Seconds

A contact list page — with a loop, because that's where builders shine over templates:

from genro_builders.builders import HtmlBuilder

contacts = [
    ("Giovanni", "giovanni@example.com"),
    ("Maria", "maria@example.com"),
    ("Luca", "luca@example.com"),
]

builder = HtmlBuilder()
body = builder.source.body()
body.h1("Contacts")

table = body.table()
header = table.tr()
header.th("Name")
header.th("Email")

for name, email in contacts:
    tr = table.tr()
    tr.td(name)
    tr.td(email)

builder.build()
print(builder.render())

Output:

<body>
  <h1>Contacts</h1>
  <table>
    <tr><th>Name</th><th>Email</th></tr>
    <tr><td>Giovanni</td><td>giovanni@example.com</td></tr>
    <tr><td>Maria</td><td>maria@example.com</td></tr>
    <tr><td>Luca</td><td>luca@example.com</td></tr>
  </table>
</body>

Key insight: this is plain Python — loops, conditionals, variables. No template language, no string concatenation. The builder validates the structure as you go: th can only go inside tr, tr can only go inside table. Try putting a td directly in body and you get an error immediately.

As a reusable class (via BuilderManager)

from genro_builders import BuilderManager

class HtmlManager(BuilderManager):
    def __init__(self):
        self.page = self.set_builder('page', HtmlBuilder)
    def render(self):
        return self.page.render()

class ContactPage(HtmlManager):
    def __init__(self):
        super().__init__()
        self.setup()
        self.build()

    def store(self, data):
        data['contacts'] = [
            {'name': 'Giovanni', 'email': 'giovanni@example.com'},
            {'name': 'Maria', 'email': 'maria@example.com'},
        ]

    def main(self, source):
        body = source.body()
        body.h1("Contacts")
        self.contact_table(body)

    def contact_table(self, parent):
        table = parent.table()
        header = table.tr()
        header.th("Name")
        header.th("Email")
        for c in self.reactive_store['contacts']:
            tr = table.tr()
            tr.td(c['name'])
            tr.td(c['email'])

page = ContactPage()
print(page.render())

Same output — but now the page is a class you can subclass, test, and reuse. The builder is a machine; the manager defines what to build.

The Three Decorators

A builder's grammar is defined with three decorators. Each decorator marks a method on the builder class:

@element — a pure tag

Declares that a tag exists and what it can contain. The body must be empty (...).

@element(sub_tags="")           # leaf: no children allowed
def ingredient(self): ...

@element(sub_tags="pasta, risotto")  # container: these children only
def first_courses(self): ...

@element(sub_tags="*")            # wildcard: any children
def div(self): ...

@abstract — inheritance base

Cannot be used directly. Defines sub_tags that concrete elements inherit.

@abstract(sub_tags="ingredient, note")
def base_dish(self): ...

@element(inherits_from="@base_dish")   # inherits sub_tags
def pasta(self): ...

@component — reusable structure

Has a body with logic. The body is NOT executed at call time — only during build(), lazily.

@component(sub_tags="")
def soffritto(self, comp, **kw):
    comp.ingredient("onion")
    comp.ingredient("carrot")
    comp.ingredient("celery")

Lazy expansion: when you call source.soffritto(), only a placeholder node is created. At build() time, the body runs and the three ingredients appear. The source stays a clean "recipe" until materialization.

sub_tags cardinality

SyntaxMeaning
""Leaf — no children allowed
"*"Any children
"a, b"Only a and b (any count)
"a[1]"Exactly 1 of a
"a[:2]"0 to 2 of a
"a[1:3]"1 to 3 of a

Return value: leaf vs container

# h1 is leaf (sub_tags="") → returns PARENT → p is sibling
source.h1("Title").p("Subtitle")

# div is container (sub_tags="*") → returns SELF → p is inside
div = source.div(id="main")
div.p("Inside the div")

The Manager Pattern — store() / main()

A builder is a machine — it knows how to build, not what. To define the content declaratively, use a BuilderManager. The three-level hierarchy: manager mixin → specialized manager → your app.

from genro_builders.builders import MarkdownBuilder
from genro_builders import BuilderManager

class MdManager(BuilderManager):
    def __init__(self):
        self.doc = self.set_builder('doc', MarkdownBuilder)
    def render(self):
        return self.doc.render()

class MonthlyReport(MdManager):
    def __init__(self):
        super().__init__()
        self.setup()       # store → main
        self.build()       # source → built

    def store(self, data):
        # Populate data. Called by setup().
        data['title'] = 'January 2026 Report'
        data['author'] = 'Giovanni'

    def main(self, source):
        # Entry point: build the tree. Called by setup() after store().
        source.h1(value='^title')
        source.p(value='^author')
        self.sections(source)

    def sections(self, source):
        # Helper method — organize as you like
        source.h2('Sales')
        source.p('Sales increased by 15%.')

report = MonthlyReport()
print(report.render())

The manager IS the document. store() and main() live on the manager, not on the builder. The builder is a machine that materializes and renders. setup() calls store → main. build() materializes. render() produces output.

Both are optional

What you defineWhat happens
main() onlySource populated from main, data populated manually
store() onlyData populated, source populated manually from outside
Bothsetup() calls store()main()
NeitherPopulate manually: app.page.source.h1("...")

node_id: find nodes directly

builder = HtmlBuilder()
builder.source.div(node_id='header').h1('Title')
builder.source.div(node_id='content').p('Body')
builder.build()

header = builder.node_by_id('header')   # O(1) dict lookup

Duplicate node_id raises ValueError.

^Pointers: Data Binding

A ^pointer connects a node in the tree to a value in the data store. At build() time, pointers are resolved. After subscribe(), changing the data triggers automatic re-rendering.

builder.data['user'] = 'Giovanni'
builder.source.h1(value='^user')
builder.build()
builder.subscribe()    # activate reactivity
# h1 shows "Giovanni"

builder.data['user'] = 'Marco'
# h1 automatically updates to "Marco"

Three path forms

SyntaxTypeResolves to
^user.nameAbsolutedata['user.name']
^.nameRelativeFrom this node's datapath ancestor chain
^#header.titleSymbolicFind node_id='header', use its datapath

Relative paths with datapath

outer = source.div(datapath='customer')
outer.p(value='^.name')      # resolves to ^customer.name
outer.p(value='^.email')     # resolves to ^customer.email

Symbolic paths with #node_id

source.div(node_id='addr', datapath='customer.address')

# Anywhere else in the tree:
source.span(value='^#addr.street')  # resolves to ^customer.address.street

abs_datapath() resolves any form to absolute: node.abs_datapath('.name') returns the full path. Works with absolute, relative, and symbolic paths.

Attribute pointers

# Read attribute 'color' from data node 'theme.button'
source.div(bg='^theme.button?color')

Renderers: Serialized Output

A renderer transforms the built Bag into a string (HTML, Markdown, XML, etc.). It's "dead" output — no live objects, no reactivity in the result itself.

Two styles

from genro_builders.renderer import BagRendererBase, renderer

class MyRenderer(BagRendererBase):

    # Declarative: empty body + template
    @renderer(template="# {node_value}")
    def h1(self): ...

    # Logic: method receives (self, node, ctx)
    @renderer()
    def table(self, node, ctx):
        lines = []
        for row in node.value:
            cells = [str(c.static_value) for c in row.value]
            lines.append("| " + " | ".join(cells) + " |")
        return "\n".join(lines)

Template variables

The context (ctx) passed to handlers and templates contains:

VariableContent
{node_value}The node's value as string
{node_label}The node's label
{children}Rendered children joined together
{any_attr}Any node attribute (e.g. {href}, {lang})

Register on builder

class MyBuilder(BagBuilderBase):
    _renderers = {'markdown': MyRenderer}

Compilers: Live Objects

A compiler produces live objects — Textual widgets, openpyxl workbooks, GUI elements. Same concept as renderer, different output type.

from genro_builders.compiler import BagCompilerBase, compiler

class TextualCompiler(BagCompilerBase):

    # Declarative: module + class to instantiate
    @compiler(module="textual.widgets", cls="Button")
    def button(self): ...

    # Logic: build the object manually
    @compiler()
    def datatable(self, node, ctx):
        return build_datatable(node)

    def compile(self, built_bag, target=None):  # must override
        return list(self._walk_compile(built_bag))

Register on builder

class MyBuilder(BagBuilderBase):
    _renderers  = {'html': HtmlRenderer}
    _compilers  = {'textual': TextualCompiler}

# Use:
builder.build()                       # materialize source → built
html = builder.render()               # string output
html = builder.render(output='page.html')  # with destination
widgets = builder.compile(name='textual')  # live objects
widgets = builder.compile(name='textual', target=container)  # with target

render() = strings. compile() = objects. A builder can have both. Multiple renderers and compilers are supported — select by name.

BuilderManager: Multiple Builders, Shared Data

When you need the same data to produce different outputs (HTML + PDF, or page + sidebar), a BuilderManager coordinates multiple builders with a shared data store.

from genro_builders import BuilderManager

class MyReport(BuilderManager):
    def __init__(self):
        self.html = self.set_builder('html', HtmlBuilder)
        self.pdf  = self.set_builder('pdf', PdfBuilder)
        self.setup()   # store → main_html → main_pdf
        self.build()   # materialize all builders

    def store(self, data):
        # Shared data — all builders see this
        data['title'] = 'Q1 Report'
        data['author'] = 'Giovanni'

    def main_html(self, source):
        source.h1(value='^title')
        source.p(value='^author')

    def main_pdf(self, source):
        source.title(value='^title')
        source.subtitle(value='^author')

report = MyReport()

Data namespaces

PointerReads from
^titleShared store root
^.subtitleBuilder's private namespace (builders.html.subtitle)

Putting It All Together

A complete custom builder: grammar + renderer + subclass usage.

from genro_builders import BagBuilderBase
from genro_builders.builders import element, component
from genro_builders.renderer import BagRendererBase, renderer


# ── 1. Grammar ──

class InvoiceBuilder(BagBuilderBase):

    @element(sub_tags="line_item, note")
    def invoice(self): ...

    @element(sub_tags="")
    def line_item(self, description: str, qty: int = 1,
                  price: float = 0): ...

    @element(sub_tags="")
    def note(self): ...

    @component(sub_tags="")
    def standard_footer(self, comp, **kw):
        comp.note("Payment within 30 days")
        comp.note("VAT included")


# ── 2. Renderer ──

class InvoiceRenderer(BagRendererBase):

    @renderer(template="=== INVOICE ===\n{children}")
    def invoice(self): ...

    @renderer()
    def line_item(self, node, ctx):
        return f"  {ctx['description']} x{ctx['qty']} @ EUR {ctx['price']}"

    @renderer(template="  NOTE: {node_value}")
    def note(self): ...

InvoiceBuilder._renderers = {'text': InvoiceRenderer}


# ── 3. Manager + App ──

class InvoiceManager(BuilderManager):
    def __init__(self):
        self.inv = self.set_builder('inv', InvoiceBuilder)
    def render(self):
        return self.inv.render()

class MyInvoice(InvoiceManager):
    def __init__(self):
        super().__init__()
        self.setup()
        self.build()

    def store(self, data):
        data['client'] = 'Acme Corp'

    def main(self, source):
        inv = source.invoice()
        inv.line_item(description="Widget A", qty=10, price=5.99)
        inv.line_item(description="Widget B", qty=3, price=12.50)
        inv.standard_footer()

invoice = MyInvoice()
print(invoice.render())
InvoiceBuilder grammar InvoiceRenderer @renderer handlers MyInvoice setup() + build() output text string registers extends build()

Cheat Sheet

I want to…

I want to…How
Create a builderclass MyBuilder(BagBuilderBase): ...
Define a tag@element(sub_tags="child1, child2")
Define a leaf tag@element(sub_tags="")
Define a wildcard container@element(sub_tags="*")
Inherit sub_tags from abstract@element(inherits_from="@base")
Create a reusable component@component(sub_tags="") with body
Limit where a tag can go@element(parent_tags="ul,ol")
Limit children countsub_tags="item[1:3]" (1 to 3)
Map multiple tags to one handler@element(tags="wine, beer, juice")
Bind a node to datasource.h1(value='^title')
Use relative data pathsource.div(datapath='user').p(value='^.name')
Use symbolic data pathsource.span(value='^#header.title')
Identify a node for lookupsource.div(node_id='header')
Find a node by idbuilder.node_by_id('header')
Resolve any path to absolutenode.abs_datapath('.name')
Define a template renderer@renderer(template="# {node_value}")
Define a logic renderer@renderer() + method body
Define a compiler handler@compiler(module="x", cls="Y")
Register renderer on builder_renderers = {'name': MyRenderer}
Register compiler on builder_compilers = {'name': MyCompiler}
Render to a file/streambuilder.render(output='page.html')
Compile with a target containerbuilder.compile(target=parent_widget)
Coordinate multiple buildersSubclass BuilderManager + set_builder()
Set shared data (manager)Override store(data) on manager
Build each builder's sourceOverride main_html(source), main_pdf(source) on manager
Validate structurebuilder._check(bag)
Validate attribute typesType hints + Annotated[str, Regex(...)]
Rebuild with new contentbuilder.rebuild(main=my_func)

Pipeline summary

StepWhereWhat happens
setup()ManagerCalls store(data) → main(source) to populate
build()Manager/BuilderExpand @components, resolve ^pointers (materialize)
subscribe()Manager/BuilderActivate reactive bindings (optional)
render()BuilderProduce string output via BagRendererBase
compile()BuilderProduce live objects via BagCompilerBase

Built-in builders

BuilderImportOutput
HtmlBuilderfrom genro_builders.contrib.html import HtmlBuilderHTML5
MarkdownBuilderfrom genro_builders.contrib.markdown import MarkdownBuilderMarkdown
XsdBuilderfrom genro_builders.contrib.xsd import XsdBuilderXML from XSD