Advanced Patterns

This guide covers advanced builder patterns for complex use cases.

Builder Inheritance

Extending Existing Builders

SchemaBuilder: Programmatic Schema Creation

SchemaBuilder allows you to define schemas programmatically instead of using decorators. This is useful for:

  • Dynamic schema generation

  • Schemas loaded from external sources

  • Reusable schema definitions shared across builders

Basic Usage

The item() Method

schema.item(
    name: str,              # Element name (or '@name' for abstract)
    sub_tags: str = '',     # Valid child tags with cardinality
    inherits_from: str = None,  # Abstract element to inherit from
)

Defining Abstract Elements

Use @ prefix for abstract elements:

Compiling to File

Save the schema for reuse:

# Save to MessagePack (binary, compact)
schema.builder._compile('my_schema.msgpack')

Using Compiled Schema

Load the schema in a custom builder:

from genro_builders import BuilderBag
from genro_builders.builders import BagBuilderBase

# Method 1: Class attribute
class MyBuilder(BagBuilderBase):
    _schema_path = 'my_schema.msgpack'

# Method 2: Constructor parameter
bag = BuilderBag(builder=BagBuilderBase, builder_schema_path='my_schema.msgpack')

Complete Example: Config Schema

When to Use SchemaBuilder vs @element

Approach

Use When

@element decorator

Schema is static, defined in code

SchemaBuilder

Schema is dynamic, generated at runtime

SchemaBuilder + file

Schema is shared across multiple builders

XSD → SchemaBuilder

Schema comes from external XSD file

Loading Schema from File

Builders can load schema from a pre-compiled MessagePack file using _schema_path:

from genro_builders import BuilderBag
from genro_builders.builders import BagBuilderBase

class MyBuilder(BagBuilderBase):
    _schema_path = 'path/to/schema.msgpack'  # Load at class definition

# Or pass at instantiation
bag = BuilderBag(builder=MyBuilder, builder_schema_path='custom_schema.msgpack')

Custom Validation

For custom validation logic, see Validation.

Performance Tips

1. Use Batch Operations

# Instead of validating each step:
for data in large_dataset:
    parent.item(data)

# Validate once at the end:
errors = builder.validate()

Real-World Example: Config Builder

Reactive Formulas

Data formulas let you define computed values that re-execute automatically when their ^pointer dependencies change. See Reactive Data for the full guide.

Formula Dependency Chains

When formulas depend on each other, they execute in topological order:

from genro_builders.contrib.html import HtmlBuilder

builder = HtmlBuilder()
s = builder.source

s.data_setter('base_price', value=100)
s.data_setter('discount', value=0.1)
s.data_setter('tax_rate', value=0.22)

# Executes first: depends on base_price and discount
s.data_formula('net_price',
    func=lambda base_price, discount: base_price * (1 - discount),
    base_price='^base_price',
    discount='^discount',
)

# Executes second: depends on net_price (computed above)
s.data_formula('total',
    func=lambda net_price, tax_rate: net_price * (1 + tax_rate),
    net_price='^net_price',
    tax_rate='^tax_rate',
)

s.body().p(value='^total')

builder.build()
builder.subscribe()
print(builder.output)

# Change base_price -> net_price recalculates -> total recalculates -> re-render
builder.data['base_price'] = 200
print(builder.output)

Computed Attributes

Callable attributes are resolved via 2-pass evaluation on the node. In pass 1 all ^pointer attributes are resolved to values. In pass 2, callables are called with matching resolved attributes as kwargs:

# Callable that uses other resolved attributes
s.body().div(
    price="^item.price",
    qty=3,
    total=lambda price, qty: price * qty,
)

# Callable with ^pointer defaults (resolved from data store)
s.body().div(
    style=lambda bg='^theme.bg', fg='^theme.fg': f'background:{bg};color:{fg}',
)

Output Suspension

When making multiple data changes, use suspend_output() / resume_output() to avoid redundant re-renders:

builder.build()
builder.subscribe()

builder.suspend_output()
builder.data['a'] = 1
builder.data['b'] = 2
builder.data['c'] = 3        # formulas re-execute, but no render
builder.resume_output()       # single render with all changes applied

This is useful for initialization patterns where many data values are set at once, or for batch updates from external sources.

Debounce and Periodic Execution

Debounce with _delay

Prevent rapid re-execution with a debounce window:

s.data_formula('search_results',
    func=lambda query: api_search(query),
    query='^search.query',
    _delay=0.5,  # wait 500ms after last change
)

Periodic with _interval

Re-execute at regular intervals after subscribe():

import time

s.data_formula('clock',
    func=lambda: time.strftime('%H:%M:%S'),
    _interval=1.0,  # every second
)

Interval timers start on subscribe() and stop on rebuild or clear.