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).
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)
|
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
|