Skip to content

construction

construction #

How a node class is built and how a node instance is constructed.

Three pieces, all about turning a node class into a well-behaved frozen, keyword-only dataclass and a call into a coerced, validated node:

  • [NODE_REGISTRY][] — every node class by its JSON kind (== class name).
  • [make_procedure][] — the class decorator: frozen, keyword-only dataclass (no field equality — identity is by uuid), immutable after construction, and self-registering into NODE_REGISTRY.
  • [ProcedureMeta][] — the construction hook: for each Input field it coerces the value to a Procedure (a raw literal lifts to a Constant, a Path position used as a value resolves to the node that wrote it).

ProcedureMeta #

Bases: type

Construction hook: coerce input values on the way in.

Calling any node routes through here. A Path handed to a concrete-typed Input resolves to the procedure that wrote that slot; a generic Input[T] or an Input[Path] field keeps its position untouched.

Source code in capturegraph-lib/capturegraph/procedures/procedure/type_checking/construction.py
class ProcedureMeta(type):
    """Construction hook: coerce input values on the way in.

    Calling any node routes through here. A ``Path`` handed to a concrete-typed
    ``Input`` resolves to the procedure that wrote that slot; a generic
    ``Input[T]`` or an ``Input[Path]`` field keeps its position untouched.
    """

    def __call__[T](
        cls: type[T],
        *args: object,
        **kwargs: object,
    ) -> T:
        """Construct a validated ``cls`` instance, coercing each ``Input`` field."""
        if args:
            raise TypeError(
                f"{cls.__name__} takes keyword arguments only (got {len(args)} positional)"
            )

        from dataclasses import MISSING

        from capturegraph.procedures.procedure.authoring.bridge import coerce_input

        for name, role, annotation in field_roles(cls):
            if role is not FieldRole.INPUT:
                continue
            # Defaults coerce exactly like passed values; an absent required
            # input is left for the dataclass __init__ to flag.
            if name in kwargs:
                value = kwargs[name]
            else:
                dataclass_field = cls.__dataclass_fields__[name]  # pyright: ignore[reportAttributeAccessIssue]
                if dataclass_field.default is not MISSING:
                    value = dataclass_field.default
                elif dataclass_field.default_factory is not MISSING:
                    value = dataclass_field.default_factory()
                else:
                    continue
            kwargs[name] = coerce_input(value, resolve_path=_resolves_path_to_value(annotation))

        return super().__call__(**kwargs)

__call__(*args, **kwargs) #

Construct a validated cls instance, coercing each Input field.

Source code in capturegraph-lib/capturegraph/procedures/procedure/type_checking/construction.py
def __call__[T](
    cls: type[T],
    *args: object,
    **kwargs: object,
) -> T:
    """Construct a validated ``cls`` instance, coercing each ``Input`` field."""
    if args:
        raise TypeError(
            f"{cls.__name__} takes keyword arguments only (got {len(args)} positional)"
        )

    from dataclasses import MISSING

    from capturegraph.procedures.procedure.authoring.bridge import coerce_input

    for name, role, annotation in field_roles(cls):
        if role is not FieldRole.INPUT:
            continue
        # Defaults coerce exactly like passed values; an absent required
        # input is left for the dataclass __init__ to flag.
        if name in kwargs:
            value = kwargs[name]
        else:
            dataclass_field = cls.__dataclass_fields__[name]  # pyright: ignore[reportAttributeAccessIssue]
            if dataclass_field.default is not MISSING:
                value = dataclass_field.default
            elif dataclass_field.default_factory is not MISSING:
                value = dataclass_field.default_factory()
            else:
                continue
        kwargs[name] = coerce_input(value, resolve_path=_resolves_path_to_value(annotation))

    return super().__call__(**kwargs)

make_procedure(cls) #

Declare a procedure node class as a frozen, keyword-only dataclass.

Apply to every Procedure subclass. Field-wise equality and repr are disabled so Procedure's own versions win (__eq__ compares by uuid; __repr__ is the node's JSON); the frozen __setattr__ is replaced with one that permits the |= / &= rebind. The dataclass_transform deliberately omits frozen_default so the type checker treats nodes as mutable enough to allow path.field |= value while the runtime stays frozen.

Source code in capturegraph-lib/capturegraph/procedures/procedure/type_checking/construction.py
@dataclass_transform(
    kw_only_default=True,
    field_specifiers=(field,),
)
def make_procedure[T: Procedure[CGType]](cls: type[T]) -> type[T]:
    """Declare a procedure node class as a frozen, keyword-only dataclass.

    Apply to every ``Procedure`` subclass. Field-wise equality and ``repr`` are
    disabled so ``Procedure``'s own versions win (``__eq__`` compares by ``uuid``;
    ``__repr__`` is the node's JSON); the frozen ``__setattr__`` is replaced with
    one that permits the ``|=`` / ``&=`` rebind. The ``dataclass_transform``
    deliberately omits ``frozen_default`` so the type checker treats nodes as
    mutable enough to allow ``path.field |= value`` while the runtime stays frozen.
    """
    cls = dataclass(
        kw_only=True,
        frozen=True,
        eq=False,
        repr=False,
    )(cls)
    cls.__setattr__ = _immutable_setattr
    if cls.__name__ != "Procedure":
        NODE_REGISTRY[cls.__name__] = cls
    return cls