The rule cascade¶
Screen(rules=...) is declui's app sheet — the MetaUI idea of setting a field's presentation by selector. A rule matches a field's context and applies Field options to it, so one line can shape many fields at once, or vary one model by screen. Like everything in declui, it resolves once, at mount time — there is no runtime rule engine.
from situ.declui import Rule, Screen
ISSUE = Screen(
model=Issue,
rules=(
Rule({"type": "Priority"}, {"widget": "Popup"}), # every Priority field
Rule({"field": "subject", "operation": "list"}, {"link": "detail_url"}), # subject links, list only
Rule({"field": "notes", "operation": "create"}, {"hidden": True}), # hide notes on the create form
Rule({}, {"editable": "not locked"}), # a screen-wide predicate
),
)
A rule doesn't add a second vocabulary — it sets the same Field options you'd write inline, but by selector and per operation. Rule({"type": "Status"}, {"widget": "Popup"}) is "set Field(widget="Popup") on every Status-typed field."
Ranks¶
Each field resolves from three ranked sources, folded into one effective Field:
| Rank | Source | Example |
|---|---|---|
| 0 | introspection — the field's type → widget default | str → text input |
| 1 | field metadata — the field's own Field(...) |
Field(label="Subject") |
| 2 | the app sheet — matching Screen.rules |
Rule({"type": "Status"}, {"widget": "Popup"}) |
Higher rank wins, so a rule (rank 2) overrides the field's own Field (rank 1). Within rank 2, a more specific rule (more selector keys) wins, and ties break by declaration order (later wins).
The context and the selectors¶
A rule matches against a field's context: {model, field, type, operation}, where operation is the screen kind being generated — form / create / edit / list / detail. A selector matches when all its entries match, in one of three arities:
Rule({"field": "title"}, {...}) # equality — the field named "title"
Rule({"type": "Status"}, {...}) # equality — every Status-typed field
Rule({"operation": ["list", "detail"]}, {...}) # set-membership — either operation
Rule({"field": ...}, {...}) # presence — any field (Ellipsis)
Rule({}, {...}) # empty — every field (a screen-wide rule)
The operation axis is what one model can't express through Field() alone: a Field(hidden=True) hides a field on every screen, but Rule({"field": "notes", "operation": "create"}, {"hidden": True}) hides it on the create form only.
Override properties vs predicates¶
Properties merge by a fixed policy:
- Override —
widget/label/hidden/link/format/source/search/badge/required/secret/label_field: the highest-ranked, most-specific rule replaces the value. - Predicate —
visible/editable/valid: every matching rule's predicate AND-chains with the field's own, so a field is visible only when all of them hold.Screen.object_editableis the same idea (a screen-wideeditableAND-ed into every field).
# body is editable only when its own rule AND the screen-wide rule both hold
Rule({}, {"editable": "not locked"}), # screen-wide
Rule({"field": "body"}, {"editable": "len(title) > 3"}),
# -> body editable = "(len(title) > 3) and (not locked)"
Force — escaping a chain¶
Force("...") sets a predicate unconditionally, discarding the lower / less-specific predicates it would otherwise AND with (situ's stand-in for MetaUI's !). Use it to re-enable one field a screen-wide rule disabled:
Rule({}, {"editable": "not locked"}), # locks every field
Rule({"field": "notes"}, {"editable": Force("True")}), # …except notes
explain — see how a field resolves¶
Because the whole cascade is a pure function over the Screen, you can print a field's derivation from the command line — no runtime inspector:
$ python -m situ.declui explain myapp.screens:ISSUE status detail
Issue.status @ detail
context: {'model': 'Issue', 'field': 'status', 'type': 'Status', 'operation': 'detail'}
rank 0 (type): Status
rank 1 (Field): {'label': 'Status'}
rank 2 (rules), least specific first:
[0] specificity 1 {'type': 'Status'} -> {'widget': 'Popup'}
[1] specificity 2 {'field': 'status', 'operation': 'detail'} -> {'label': 'État', 'widget': 'Combobox'}
effective:
label = 'État' <- changed by a rule
widget = 'Combobox' <- changed by a rule
The situ.declui.explain(screen, field, operation) function backs the CLI if you want the trace in a test.
Fail-closed¶
Rules are checked once per screen at mount:
- an unknown selector key (not
model/field/type/operation) →CompileError; - a
fieldselector naming a field the model lacks (a typo that would silently match nothing) →CompileError; - a predicate value that isn't a string or
Force, or an unknown property →CompileError.
The line, restated¶
A rule sheet fatter than the markup it would replace is the signal to eject that screen. The cascade owns the boring 80% — one model → a working screen with per-selector, per-operation defaults — and bespoke markup stays yours in plain situ. It deletes boilerplate, not design.