Shapes
An addon for document-model data. Ships two inseparable things: a DSL (Shape + Slot) for declaring structured shapes, and the structured-Ref blueprints those shapes live on. The DSL exists to wire up the Refs -- you can't usefully have one without the other.
Why not core
Nu's Ref is atomic: one name, one fabric slot. Shapes is for when you have nested structured data and want typed paths into it -- navigation that knows the structure and catches "no such field" / "wrong index type" at composition time. That's a DX layer on top of Refs, not part of the core algebra.
What the DSL gives you
Shapeclass +Slotdescriptors -- declare a document shape as a Python class.- From a
Shapedefinition,ShapeRefis derived automatically. Its.attr/[k]/[i]navigation produces correctly-typed child Refs into the same fabric. - Composition is recursive: a
Shapewhose Slot holds anotherShapechains navigation depth-first.
Slot declaration surface
Annotation is authoritative. The annotation IS the Ref class (bare or parametric); ShapeMeta synthesizes a Slot at class-definition time and stamps a recursive TypeInfo onto the created Ref's payload. .slot(...) stays around for config (view=, size=, capacity=, ...) or as a legacy positional path.
class Profile(nu.Shape):
# leaf refs: annotation only, no .slot()
name: nm.StrRef
age: nm.IntRef
# nested single shape: annotation is the Shape (for dot-nav
# autocomplete over its slots). Requires explicit .slot() because
# bare Shape can't name the fabric.
address: Address = nm.ShapeRef.slot(Address)
# primitive-blob collections: raw python primitive inside
tags: nv.PrimitiveListRef[str]
meta: nv.PrimitiveDictRef[str, int]
# decomposed shape collections: each element is its own ShapeRef
orders: nm.ShapesDictRef[int, Order]
history: nm.ShapesListRef[Event]
# with config: annotation + .slot(config)
top10: nv.Kh57Ref[Order] = nv.Kh57Ref.slot(sample_size=10)Annotation rules
Three shapes an annotation can take:
- Bare Ref class (
nm.StrRef,nd.HeadingRef) - metaclass createsSlot(<cls>). - Parametric Ref class (
nv.PrimitiveListRef[str],nm.ShapesDictRef[int, Order]) - metaclass createsSlot(<origin>, **derived_kwargs)where kwargs come from the origin's_slot_kwargs_from_type_argsclassmethod. - Bare Shape subclass (
Order) - shorthand for aShapeRefnavigating that shape. Requires explicit= <fabric>.ShapeRef.slot(Order)because the annotation alone can't name the fabric. Gives dot-nav autocomplete overOrder's own slots.
Type args discipline (level-2 typing)
Raw python primitives (str, int, bytes, ...) appear ONLY as type args of Primitive*Ref (whole-container blob storage). Decomposed containers (ListRef, DictRef, Kh57Ref, ...) take Ref classes or Shape subclasses as type args - each element is its own addressable ref.
# primitive: raw python type inside
tags: nv.PrimitiveListRef[str]
# decomposed: refs / shapes inside
orders: nm.ShapesDictRef[int, Order]
queue: nv.ListRef[Order]
queuerel: nv.ListRef[nm.DictRef[str, Order]] # chained decompositionTypeInfo
The recursive carrier lives at nu.lang.typeinfo.TypeInfo(py_type, key, elem). It's what gets stamped on the ref payload and read by every wrapper. py_type legitimately carries a Ref subclass, a Shape subclass, a Python primitive, or Any (the honest fallback for non-trivial unions and unresolvable forward refs). Accessors: is_ref / is_shape / is_primitive / is_any. to_form(tier) dispatches primitive types to their Form class (single source of truth for the type -> Form map).
T | None collapses to T. Non-trivial unions collapse to Any. Forward refs resolve lazily via typing.get_type_hints (memoized).
Structured-Ref families
Seven families, three tiers each. The blueprint a fabric exposes depends on what the backend supports.
| Family | Base (read) | Mutable (write) | Reactive (write + observe) |
|---|---|---|---|
| Item | ItemRef | MutableItemRef | ReactiveItemRef |
| Mapping | MappingRef | MutableMappingRef | ReactiveMappingRef |
| Sequence | SequenceRef | MutableSequenceRef | ReactiveSequenceRef |
| Set | SetRef | MutableSetRef | ReactiveSetRef |
| Shape | ShapeRef | MutableShapeRef | ReactiveShapeRef |
| ShapesMapping | ShapesMappingRef | MutableShapesMappingRef | ReactiveShapesMappingRef |
| ShapesSequence | ShapesSequenceRef | MutableShapesSequenceRef | ReactiveShapesSequenceRef |
7 families × 3 tiers = 21 Ref blueprints.
Tier surface
| Tier | Methods |
|---|---|
| Base | exists, missing, extract; Form surface: iter, len, contains, [k]/[i]/.attr |
| Mutable | adds store, erase, init |
| Reactive | adds on_change, on_child_change, on_children_change, on_descendants_change |
Form composition
Each Ref tier composes with the matching Form tier:
class MappingRef(MappingForm, StructuredRef): ...
class MutableMappingRef(MutableMappingForm, MappingRef): ...
class ReactiveMappingRef(ReactiveMappingForm, MutableMappingRef): ...StructuredRef is the hierarchical base every family extends - the shared navigation toolkit. It is public: fabrics implement their Refs against it.
Form mixins live in nu2/forms/ (collections/abc + abc). Refs in nu2/domains/shape/refs/. Repo root for this domain: nu2/domains/shape/.
The on_change* family in the Reactive tier is not shape-specific - those queries live in nu2/forms/reactive.py (generic Form-layer infra). Reactive Refs pick them up by composing with ReactiveForm tiers from nu2/forms/.
Fabric examples
nu/mem-- in-memory writes, no native events -- exposesMutable*blueprints.nu/virtuals-- writes + change events -- exposesReactive*blueprints.- read-only snapshot -- no writes, no events -- exposes base blueprints.
Address structure
A hierarchical Ref holds its parent in the tree, not off to the side:
children[0]= the parent Ref, markedstructural- address structure, resolved but never value-read.children[1:]= the keys - ordinary value slots, evaluated and read.
You write a path outside-in (user.profile.email); the tree holds it inside-out - email on top, user deepest in the children[0] chain. A Ref used as a key (tokens[current]) sits in a value slot, so it reads normally; its own parent stays structural. The role is a property of the (node, slot) edge, so the same Ref is never both structural and value. A flat Ref (one address, no hierarchy) declares structural = frozenset() and keeps its shape - no parent slot, no sentinel.
Because the parent lives in children, every tree pass reaches the whole Ref uniformly. No off-tree parent link, no with_children copy tax, no separate walk.
Substrate protocol
A Ref blueprint is abstract until wired to a fabric. The contract is a named Protocol - non-underscore names, pinned signatures - split in two.
Core - every Ref, every fabric. One method:
resolve_address(rt, nid)/aresolve_address- resolve the address by recursingchildren[0](structural) and evaluatingchildren[1:](value).
Capabilities - declared per fabric, dispatched at compile. The verbs a fabric supports:
| Fabric | Capabilities |
|---|---|
| mem, virtuals | load/aload, write/awrite, erase/aerase, materialize/amaterialize |
| nudle | store, append, changed - async-only |
| io | write, readline |
| ctx (FabricRef) | read-only; the read self-yields the bound fabric. Calls are separate method atoms via MethodFactory. |
load is the dual-role value fetch; materialize is the write-path fetch-or-create of the parent container (vivification). A fabric missing a capability fails at compile, not with a runtime error.
Value boundary. One symmetric pair on the Ref's value type:
lift- raw storage value to typed value (DateRefstr todate). Runs on every read path.lower- typed value to storage form.store/writetakes one canonical typed input.
Vocab
Shapeis canonical.schemais banned (DB connotation; see vocab.md).Slotis canonical.fieldis banned.ShapeRefis the derived Ref a Shape produces.StructuredRefis the public hierarchical Ref base every family extends.