Node settings arrive as keyword arguments, not a params dict

A python node's settings are constants of its own function, so they are passed
the way its ports are: by name. The controller binds them to the compiled
function, the `params` field is gone from the worker and remote protocols, and
a setting sharing a port's name is reported as a node error rather than
shadowing it. The panel's scaffold follows suit and keeps the header in step
with both ports and settings.

The demo's `pace` moves from a flow input to a setting of the training node,
which is what it always was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
This commit is contained in:
2026-08-20 17:47:45 +02:00
co-authored by Claude Opus 5
parent 2385e3cf8e
commit 4355c917f8
24 changed files with 225 additions and 126 deletions
+63
View File
@@ -0,0 +1,63 @@
"""A node's settings are arguments of its function, like its ports.
What distinguishes them is where the value comes from: a port carries whatever
the graph last published, a setting is a constant stored with the flow. Both
arrive by name, so one name cannot mean both.
"""
from pathlib import Path
import pytest
from app.flow.controller import FlowController
from app.flow.messages import DType, MessageSpec
from app.flow.pipeline import Pipeline
from app.flow.schemas import FlowDef, NodeDef
from app.flow.state import MemoryState
from app.flow.store import FlowStore
SOURCE = "def process(reading, factor):\n return {'scaled': reading * factor}\n"
def a_flow(**params: object) -> FlowDef:
return FlowDef(
name="house",
nodes=[
NodeDef(
id="scale",
params=dict(params),
requires=[MessageSpec(name="reading", dtype=DType.FLOAT)],
provides=[MessageSpec(name="scaled", dtype=DType.FLOAT)],
)
],
)
@pytest.fixture
def store(tmp_path: Path) -> FlowStore:
return FlowStore(tmp_path / "flows")
def test_a_setting_reaches_the_function_as_a_keyword_argument(store: FlowStore):
store.write_flow(a_flow(factor=3))
store.write_node_source("house", "scale", SOURCE)
controller = FlowController(store)
nodes, _loaded, _initial, _inputs = controller._build_flows(
[(store.read_flow("house"), False)]
)
pipeline = Pipeline(nodes=nodes, state=MemoryState())
pipeline.run({"house.reading": 2.0})
assert pipeline.values()["house.scaled"]["value"] == 6.0
def test_a_setting_named_after_a_port_is_refused(store: FlowStore):
store.write_draft(a_flow(reading=3), 0)
store.write_node_source("house", "scale", SOURCE, draft=True)
controller = FlowController(store)
preview = controller.preview("house")
assert [node.status for node in preview.nodes] == ["error"]
assert "both an input and a setting" in (preview.nodes[0].error or "")