Decorators
Last Updated: 2026-06-10 Status: 🟢 APPROVATO — allineato al contratto v0.8.0.
The grammar decorators live in src/genro_builders/builder/_decorators.py and are imported as:
from genro_builders.builder import (
element, abstract, container, component,
)
At a glance
Decorator |
Purpose |
Body |
|---|---|---|
|
Declare a tag in the grammar. |
Ignored (declarative). |
|
Declare an abstract base for inheritance. |
Ignored (declarative). |
|
A reusable construction block, invocable from a node. |
Runs (builds). |
|
A named grammar element with a body, expanded ephemerally at render time. Bare decorator (no arguments). The element is then called in three forms — explicit params, |
Runs (builds). |
There is no separate @subbuilder or @data_element decorator.
Both are ordinary @element declarations marked in their _meta:
a sub-builder (dialect boundary) is
@element(_meta={"subbuilder": "<dialect>", ...});a data-element (
data_setter,data_formula,data_controller) is@element(_meta={"data_element": True}).
See the dedicated sections below.
@element
Declares a concrete tag. The tag is the method name — there is no
tags argument.
@element(sub_tags='h1,p[]')
def body(self): ...
@element(sub_tags='', parent_tags='ul,ol')
def li(self): ...
A tag that is a Python keyword
When the tag clashes with a Python keyword (del, class, for…), name
the method with a trailing underscore: the renderer strips it on emission.
@element(sub_tags='*')
def del_(self): ... # the markup is <del>...</del>
For a tag a method name cannot spell at all — a hyphen or a colon
(order-line, xsl:for-each) — declare the emitted tag explicitly via
_meta={'render_tag': '...'} (see Sub-builders below); the method keeps
a valid name, the renderer emits the real tag.
sub_tags syntax:
'a,b,c'—a,b,ceach allowed any number of times (0..N).'a[2],b[0:]'—aexactly twice,bzero or more times.''(empty) — leaf, no children allowed (void element).'*'— any tag allowed (catch-all).
A bare name is unbounded (0..N); the foo[] form is not valid and
raises ValueError — use the plain name foo for 0..N, or foo[n]
for an exact count.
parent_tags (optional) — comma-separated list of valid parents.
The element can only appear under one of these tags.
inherits_from — name of an abstract element whose sub_tags are
inherited.
node_label (optional) — a fixed default label for a singleton element,
so the node is reachable by a stable key instead of an auto-generated one
(body_0, body_1…). HTML’s body and head use it. The caller’s
node_label= argument, if given, overrides this default.
@element(sub_tags='...', node_label='body')
def body(self): ...
# page.source['body'] — not 'body_0'
collection_key (optional) — declares the element a collection: each
child is labelled by its natural key instead of an auto-label. The value
is either a child attribute name, or a ${...} template over child
attributes. It is strict — a missing attribute, a duplicate key among
siblings, or an explicit node_label= on a child all raise.
@element(sub_tags='database', collection_key='code')
def databases(self): ...
db = root.databases()
db.database(code='maindb', name='abh_878')
db.database(code='logistic', name='logadelby')
# db['maindb'], db['logistic'] — not database_0, database_1
# template form: collection_key='${code}_${env}' -> label 'maindb_prod'
To point at a default member, pass an ordinary attribute on the collection node naming the chosen key — there is no special framework parameter, it is a plain attribute your application reads:
db = root.databases(default='maindb') # 'default' is just an attribute
# the application reads node.attr['default'] to pick the default member
_meta (optional) — a dict of metadata attached to the schema entry.
The framework reads _meta["data_element"] to recognise a data-element
(see below); dialects may carry their own keys.
@abstract
Declares a base for inheritance. Never instantiated directly.
@abstract(sub_tags='span,a,em,strong')
def phrasing(self): ...
@element(inherits_from='phrasing')
def p(self): ...
Abstracts live in a dedicated _abstracts sub-bag of the class
schema, separate from the top-level elements / subbuilders. Labels are
bare names — no @ prefix. The concrete element references them via
inherits_from='<name>'.
If inherits_from names an abstract that does not exist (including
typos in comma-separated lists like 'phrasing,flow'), a
ValueError is raised at class definition time. This catches
mistakes immediately instead of producing elements with silently
missing sub_tags.
Sub-builders
A tag that opens a different grammar is an ordinary @element marked
_meta={"subbuilder": "<dialect>"} — there is no @subbuilder
decorator:
class Html5Extensions:
@element(_meta={"subbuilder": "svg"})
def svg(self): ...
From <svg> down, the active builder becomes the svg dialect. The
sub-builder governs its own sub_tags; the host only declares
parent_tags (where the sub-builder may appear). At render time the
polymorphic dispatch picks the sub-builder’s renderer_<mode> (see
contract BLD.3 / HND.3); the boundary node may carry a
render_tag/render_attributes envelope in its _meta (e.g. SVG
hosting HTML in <foreignObject xmlns="...">).
Data-elements
Three transparent elements live in the source tree but emit no
markup at render time. They carry data behaviour (seeding the data
bag, computing, side effects) executed by the builder’s compute. They are
declared once on BuilderBase as ordinary @element marked
_meta={"data_element": True} and injected into every dialect’s schema
by __init_subclass__ — so every builder has them without re-declaring:
class BuilderBase(...):
@element(_meta={"data_element": True})
def data_setter(self, destination: str, value: Any): ...
@element(_meta={"data_element": True})
def data_formula(self, destination: str, func: str | Callable, **kwargs): ...
@element(_meta={"data_element": True})
def data_controller(self, func: str | Callable, **kwargs): ...
The kind is the tag name (node.node_tag); there is no kind
parameter. The three kinds differ by graph role and output:
Tag |
Signature |
Role |
|---|---|---|
|
|
seed — writes |
|
|
computed — writes the return of |
|
|
side effect — |
Used near the structure, inside main():
def main(self, root):
body = root.body(datapath="x")
body.data_setter("price", 100)
body.data_setter("tax", 0.22)
body.data_formula("total", func="compute_total",
price="^price", tax="^tax")
body.p("^total")
destination(1st field ofdata_setter/data_formula) — where the result goes, absolute or relative (".total"is composed viaabs_datapath, like a pointer).data_controllerhas nodestination.value(ofdata_setter) — kept as a flat attribute (not the node value), so aBagpayload is not captured into the source tree.func— the canonical form is a@staticmethodname (func="compute_total"), resolved left-to-right over the builder’sdata_logicsources; a miss raisesAttributeError, a non-static match raisesTypeError. An inline callable is also accepted (handy, but makes the page non-serializable and is not cross-language).bindings(kwargs) — the^pointerinputs, always explicit, resolved viaruntime_valuesand passed tofuncby name.data_formula’sfuncis pure (func(**bindings));data_controller’s receives the node first (func(node, **bindings))._on_start(formula/controller) — requests execution at the first calculation increate();data_setteralways seeds at create.
At runtime the dispatch goes through the same _command_on_node as any
element; element_call recognises the _meta['data_element'] mark, maps
the positional args onto the field names, and flags the node
_is_data_element (which the renderer skips).
Status: the data-elements run at first calculation (during
create()) and recompute in a single wave when a dependency mutates (contractDAT.4, slice 1). The multi-wave cascade (slice 2) is not yet implemented — see theRXarea of the contract androadmap/reactivity/data-elements.md.
Declarative bodies
Both decorators are declarative: the framework only reads the
signature and metadata, the function body is discarded. For
@element/@abstract, a non-empty body emits a warning at class
definition time — use ... (ellipsis) as the body to suppress it.
The data-elements and sub-builders, being @element, follow the same
rule; their behaviour lives in the builder’s compute pass / the
sub-builder dispatch, not in the body. @container and
@component are the exceptions: they carry a body that builds.