Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
63f91f8
Rebuild an Enum member after it crosses a serialization boundary
elinscott Aug 20, 2026
9401e25
Decide enum/Literal socket membership once, at assignment
elinscott Aug 20, 2026
0ef55f1
Take a task's socket contract from a Pydantic model
elinscott Aug 26, 2026
23fa73e
Check a graph task's inputs where they are resolved
elinscott Aug 26, 2026
dbbe575
Check what a call writes into a modelled socket
elinscott Aug 26, 2026
a5081bf
Pin the model contract at all three checkpoints
elinscott Aug 26, 2026
44c208b
Match the project's formatter and lint rules
elinscott Aug 26, 2026
2f2b3e6
Add the pull request description
elinscott Aug 26, 2026
7e7cbe2
Let the run-edge body say whether it ran
elinscott Aug 26, 2026
5883af0
Say what the paired controls actually are
elinscott Aug 26, 2026
82d1b15
Ask the adapter for the Python behind a wrapped value
elinscott Aug 26, 2026
f33a045
Say in the description what the adapter reading is for
elinscott Aug 26, 2026
500fc67
Compare and store what the model cannot render
elinscott Aug 26, 2026
a869aa5
Read a modelled leaf back in the type it declares
elinscott Aug 26, 2026
35fd9d2
Judge a value by the model's own config
elinscott Aug 26, 2026
0d52a0f
Compare a field the model keeps out of its dump
elinscott Aug 26, 2026
5c62a8a
Read the invariance reference before the model runs
elinscott Aug 26, 2026
af13475
Accept the class a nested field names
elinscott Aug 26, 2026
1a625b7
Check an input wherever it is written
elinscott Aug 26, 2026
e0c8bc7
Drop the pull request description from the tree
elinscott Aug 26, 2026
390a241
Check what the model contract was passing over
elinscott Aug 26, 2026
bc3fcb1
Run a field's own rule where the value is written
elinscott Aug 27, 2026
2e69779
Pin the private name a model's rules are read from
elinscott Aug 27, 2026
d6c1b1d
Check a field's rule at every write, not only the call
elinscott Aug 27, 2026
92f8538
Name the Any arm without reading its rendering
elinscott Aug 27, 2026
95381b3
Judge a field rule on what the write actually named
elinscott Aug 27, 2026
dd82e6b
Name the routes the write check is reached by
elinscott Aug 27, 2026
97b98b8
Pin the write route that reaches neither check
elinscott Aug 27, 2026
090bfe4
Judge a tagged value by what it carries, everywhere
elinscott Aug 28, 2026
c4fdd06
Name the link a graph input actually draws
elinscott Aug 28, 2026
95f8fce
Let one waiting rule wait alone
elinscott Aug 28, 2026
0c68873
Let a nested model's cross-field rule wait as well
elinscott Aug 28, 2026
13a97eb
Refuse a model this process cannot read
elinscott Aug 28, 2026
ce717e7
Say where a socket refused a value, in the error
elinscott Aug 28, 2026
a05481b
Wrap the refusal's path as black wraps it
elinscott Aug 28, 2026
bc57ee9
Hand a graph body the types its model declares
elinscott Aug 28, 2026
110a9ba
Say what the three checkpoints now do
elinscott Aug 28, 2026
4835d77
Answer a model that names itself
elinscott Aug 28, 2026
1728c9a
Bind a task's checks where its executor looks
elinscott Aug 28, 2026
c27bd10
Let a caller read a model refusal, not its text
elinscott Aug 28, 2026
4cfb5cb
Pin what an unwritten field is worth at the body
elinscott Aug 28, 2026
5e3478f
Let a write name some of a namespace's members
elinscott Aug 28, 2026
805b667
Say what the binding does, in fewer words
elinscott Aug 28, 2026
b3f9225
Rebuild a task from its stored form and write to it
elinscott Aug 28, 2026
eafeae6
Hand a body the members that were written
elinscott Aug 28, 2026
ddbd1d9
Judge a body's value by kind, not by class name
elinscott Aug 28, 2026
862b842
Hand a mapping's items over on the same terms
elinscott Aug 28, 2026
66e32bb
Let the run edge take what is passed positionally
elinscott Aug 28, 2026
c91fa5d
Let a wrapper take over the name it wraps
elinscott Aug 28, 2026
2fc6dc9
Hand a body every field, arrived or not
elinscott Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions src/node_graph/decorator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations
from typing import Any, Optional, Callable, List, Dict
from typing import Any, Optional, Callable, List, Dict, Type
import inspect
from pydantic import BaseModel
from .error_handler import ErrorHandlerSpec
from .task import Task
from .task_spec import TaskHandle, BaseHandle
Expand Down Expand Up @@ -38,6 +39,8 @@ def decorator_task(
outputs: Optional[SocketSpec | List[str]] = None,
error_handlers: Optional[Dict[str, ErrorHandlerSpec]] = None,
catalog: str = "Others",
input_model: Optional[Type[BaseModel]] = None,
output_model: Optional[Type[BaseModel]] = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Generate a decorator that register a function as a Graph task.
After decoration, calling that function `func(x, y, ...)`
Expand All @@ -49,11 +52,19 @@ def decorator_task(
catalog (str): task catalog
inputs (dict): task inputs
outputs (dict): task outputs
input_model (BaseModel): model declaring the input sockets, checked at
every call and again before the body runs
output_model (BaseModel): model declaring the output sockets and
validating the return value
"""

def wrap(func) -> TaskHandle:
from node_graph.tasks.function_task import FunctionTask
from node_graph.input_model import apply_models

in_spec, out_spec, executor = apply_models(
func, inputs, outputs, input_model, output_model
)
callable_meta = inspect_callable_metadata(func)
metadata = {"callable": callable_meta}
version = callable_meta.get("package_version")
Expand All @@ -69,11 +80,12 @@ def wrap(func) -> TaskHandle:
obj=func,
identifier=resolved_identifier,
catalog=catalog,
input_spec=inputs,
output_spec=outputs,
input_spec=in_spec,
output_spec=out_spec,
error_handlers=error_handlers,
metadata=metadata,
version=version,
executor=executor,
)

return wrap
Expand All @@ -84,6 +96,8 @@ def decorator_graph(
inputs: Optional[SocketSpec | list] = None,
outputs: Optional[SocketSpec | list] = None,
catalog: str = "Others",
input_model: Optional[Type[BaseModel]] = None,
output_model: Optional[Type[BaseModel]] = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Generate a decorator that register a function as a graph task.

Expand All @@ -92,11 +106,19 @@ def decorator_graph(
catalog (str): task catalog
inputs (dict): task inputs
outputs (dict): task outputs
input_model (BaseModel): model declaring the input sockets, checked at
every call and again when the graph is expanded
output_model (BaseModel): refused; a graph returns socket references,
which stand for values that do not exist yet
"""

def wrap(func) -> TaskHandle:
from node_graph.tasks.function_task import FunctionTask
from node_graph.input_model import apply_models

in_spec, _, executor = apply_models(
func, inputs, None, input_model, output_model, is_graph=True
)
callable_meta = inspect_callable_metadata(func)
metadata = {"callable": callable_meta}
version = callable_meta.get("package_version")
Expand All @@ -113,10 +135,11 @@ def wrap(func) -> TaskHandle:
identifier=resolved_identifier,
task_type="graph",
catalog=catalog,
input_spec=inputs,
input_spec=in_spec,
output_spec=outputs,
metadata=metadata,
version=version,
executor=executor,
)

return wrap
Expand Down
21 changes: 21 additions & 0 deletions src/node_graph/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,24 @@ class GraphDeferredIllegalOperationError(TypeError):
"""Raised when a future (Socket) is used in an illegal concrete operation."""

pass


class SocketValueError(ValueError):
"""A value the socket layer refused, carrying where it was refused.

``loc`` names the socket path below a task's inputs and ``type`` names the
rule that refused, both spelled as pydantic spells them in
``ValidationError.errors()``: a caller reading a refusal does not have to
know whether an input model or the socket layer produced it.
"""

def __init__(
self,
message: str,
*,
loc: tuple = (),
error_type: str = "",
) -> None:
super().__init__(message)
self.loc = tuple(loc)
self.type = error_type
9 changes: 8 additions & 1 deletion src/node_graph/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class ExecutorMode(str, Enum):
PICKLED_CALLABLE = "pickled_callable" # A callable serialized using cloudpickle.


#: Attribute holding the name a callable's module binds it under.
BOUND_NAME_ATTR = "__node_graph_bound_name__"


def serialize_callable(
func: Callable, register_pickle_by_value: bool = False, include_source: bool = True
) -> Dict[str, Any]:
Expand All @@ -43,7 +47,10 @@ def serialize_callable(
if not callable(func):
raise TypeError("Provided object is not a callable function or class.")
source_code = ""
callable_name = func.__name__
# A wrapper its module binds under a name of its own says so, because
# ``functools.wraps`` gives it the wrapped function's ``__name__`` and
# importing that name back would return the function, not the wrapper.
callable_name = getattr(func, BOUND_NAME_ATTR, None) or func.__name__
if func.__module__ == "__main__" or "." in func.__qualname__.split(".", 1)[-1]:
mode = ExecutorMode.PICKLED_CALLABLE
pickled_data = cloudpickle.dumps(func)
Expand Down
Loading
Loading