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.

Subclass = document

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

Your First Builder in 30 Seconds

from genro_builders.builders import MarkdownBuilder

builder = MarkdownBuilder()
builder.source.h1("My Document")
builder.source.p("A paragraph of text.")
builder.source.h2("Code Example")
builder.source.code("print('hello')", lang="python")

builder.build()
print(builder.output)

Output:

# My Document

A paragraph of text.

## Code Example

```python
print('hello')
```

Key insight: you never write output format strings. You call h1(), p(), code() — the builder knows the grammar, the renderer knows the format. Swap MarkdownBuilder for HtmlBuilder and the same structure produces HTML.

With a test

builder = MarkdownBuilder()
builder.source.h1("Title")
builder.build()
assert "# Title" in builder.output

No HTTP, no server, no mock. Just instantiate, populate, build, assert.

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 main() / store() Pattern

Instead of manipulating builder.source from outside, you subclass the builder and define two callback methods. The framework calls them in the right order.

from genro_builders.builders import MarkdownBuilder

class MonthlyReport(MarkdownBuilder):
    def store(self, data):
        # Populate data. Called first.
        data['title'] = 'January 2026 Report'
        data['author'] = 'Giovanni'

    def main(self, source):
        # Entry point: build the tree. Called 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()
report.build()
print(report.output)

The class IS the document. Subclass = reusable document template. main() is the entry point, and you can split the work across helper methods. store() sets up the data that ^pointers will resolve against.

Both are optional

What you defineWhat happens
main() onlyBuilds from main, data populated manually or via ^pointer
store() onlyData populated, source populated manually from outside
Bothstore()main() → build pipeline
NeitherClassic manual: builder.source.h1("...") then build()

node_id: find nodes directly

class MyPage(HtmlBuilder):
    def main(self, source):
        source.div(node_id='header').h1('Title')
        source.div(node_id='content').p('Body')

page = MyPage()
page.build()

header = page.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 build, changing the data triggers automatic re-rendering.

builder.data['user'] = 'Giovanni'
builder.source.h1(value='^user')
builder.build()
# 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):     # must override
        return list(self._walk_compile(built_bag))

Register on builder

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

# Use:
builder.build()                       # build + render
html = builder.render()               # string output
widgets = builder.compile(name='textual')  # live objects

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)

    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()
report.build()  # store → main_html → main_pdf → build_all

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. Use ──

class MyInvoice(InvoiceBuilder):
    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()
invoice.build()
print(invoice.output)
InvoiceBuilder grammar InvoiceRenderer @renderer handlers MyInvoice store() + main() 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}
Coordinate multiple buildersSubclass BuilderManager + set_builder()
Set shared data (manager)Override store(data)
Build each builder's sourceOverride main_html(source), main_pdf(source), etc.
Validate structurebuilder._check(bag)
Validate attribute typesType hints + Annotated[str, Regex(...)]
Rebuild with new contentbuilder.rebuild(main=my_func)

Pipeline summary

StepWhat happens
store(data)Populate the data Bag (optional)
main(source)Build the element tree (optional)
build()Expand @components, resolve ^pointers, register subscriptions
render()Produce string output via BagRendererBase
compile()Produce live objects via BagCompilerBase

Built-in builders

BuilderImportOutput
HtmlBuilderfrom genro_builders.builders import HtmlBuilderHTML5
MarkdownBuilderfrom genro_builders.builders import MarkdownBuilderMarkdown
XsdBuilderfrom genro_builders.builders.xsd import XsdBuilderXML from XSD