Source code for noob.node.spec

from __future__ import annotations

import inspect
from functools import cached_property
from typing import TYPE_CHECKING, Annotated, TypeAlias, TypedDict

from annotated_types import Len
from pydantic import BaseModel, ConfigDict, Field, field_validator

from noob.types import AbsoluteIdentifier, DependencyIdentifier, PythonIdentifier
from noob.utils import resolve_python_identifier
from noob.yaml import id_optional_json_schema

if TYPE_CHECKING:
    from noob.node.base import Signal, Slot

_DependsBasic: TypeAlias = Annotated[
    dict[PythonIdentifier, DependencyIdentifier], Len(min_length=1, max_length=1)
]
"""
Standard dependency declaration, a mapping from a node's `slot` to a `node.signal` pair:

Examples:

    for a pair of nodes like this:
    
    ```python
    def node_a() -> Annotated[Generator[int], Name("index")]:
        yield from count()
    
    def node_b(my_value: int) -> None:
        print(my_value)
    ```
    
    one would express "pass node_a.index to node_b's `my_value`" like
    
    ```yaml
    nodes:
      a:
        type: node_a
      b:
        type: node_b
        depends:
        - my_value: a.index
    ```

"""

DependsType: TypeAlias = list[DependencyIdentifier | _DependsBasic] | DependencyIdentifier
"""
Either an absolute identifier (which is treated as a positional-only arg)
or a dict mapping as described in _DependsBasic.

Examples:

    ```python
    def example(positional_only: int, /, another_arg: str) -> None:
        return another_arg * positional_only
    ```    
    
    ```yaml
    nodes:
      zzz:
        type: example
        depends:
        - a.value
        - another_arg: b.value
    ```
    
    When a dependency is a scalar value passed to the first positional argument,
    it can be specified with a scalar reference to an absolute identifier.
    For example, if one wanted to return a scalar value from a `return` node,
    specify the dependency like this:
    
    ```yaml
    nodes:
      yyy:
        type: return
        depends: a.value
    ```
    
    and to return the same value wrapped in a list...
    
    ```yaml
    nodes:
      yyy:
        type: return
        depends: 
        - a.value
    ```
    
"""


[docs] class NodeSpecification(BaseModel): """ Specification for a single processing node within a tube .yaml file. """ type_: AbsoluteIdentifier = Field(..., alias="type") """ Shortname of the type of node this configuration is for. Subclasses should override this with a default. """ id: PythonIdentifier """The unique identifier of the node""" depends: DependsType | None = None """Dependency specification for the node. Can be specified as a simple mapping from this node's input slots to another node's output signals passed as kwargs, or as a flat list of node.signal identifiers that are passed as positional args. """ params: dict | None = None """Static kwargs to pass to this node, parameterized the signature of a function node, or by a TypedDict for a class node. """ enabled: bool = True """ If this flag is False, the node will not be initialized or included in the `:meth:.Tube.graph`. """ stateful: bool | None = None """ See :attr:`.Node.stateful` , explicitly set statefulness on a node, overriding its default. If ``None`` , use the default set on the node class. """ description: str | None = None """An optional description of the node""" model_config = ConfigDict(extra="forbid", serialize_by_alias=True)
[docs] @field_validator("depends", mode="after") @classmethod def slots_unique(cls, val: DependsType | None) -> DependsType | None: """ Ensure slots are unique in dependency spec: can't map more than one signal to the same slot """ if val is None or isinstance(val, str): return val seen = set() for dep in val: if isinstance(dep, str): continue signal = next(iter(dep.keys())) if signal in seen: raise ValueError(f"Duplicate signal in dependencies: {signal}") seen.add(signal) return val
[docs] @cached_property def nodeinfo(self) -> NodeInfo: """Information about the node that this spec is for.""" from noob.node.base import Node, WrapClassNode, WrapFuncNode obj = resolve_python_identifier(self.type_) if inspect.isclass(obj): node_cls = obj if issubclass(obj, Node) else WrapClassNode else: node_cls = WrapFuncNode return NodeInfo( node_id=self.id, type=self.type_, signals=node_cls.get_signals(self), slots=node_cls.get_slots(self), )
__get_pydantic_json_schema__ = classmethod(id_optional_json_schema) # type: ignore[var-annotated]
[docs] class NodeInfo(TypedDict): """ Metadata about the completed spec given the combination of a node spec and a node class. The spec if purely static, the node class is *mostly* static, but some important properties - notably signals and slots - can be dynamic: i.e. the signals or slots of the node depend on the spec. This metadata is primarily used in visualization or inspection of tubes, rather than at runtime """ node_id: str """Node ID whose spec this NodeInfo was computed from""" type: str """fully-qualified module.object name for the node type""" signals: dict[str, Signal] """Signals computed from the spec and node class""" slots: dict[str, Slot] """Slots computed from the spec and node class"""