Header removals wait for the code editor to be left: autosave saves with drop=false, so follow_header adds and renames from the header but keeps a port or setting whose parameter is gone, and leaving the editor (blur or closing the panel) saves once more with removals; source saves of a flow run one at a time
This commit is contained in:
@@ -674,7 +674,7 @@ def read_node_source(
|
||||
|
||||
|
||||
def _follow_header(
|
||||
controller: FlowController, name: str, node_id: str, code: str
|
||||
controller: FlowController, name: str, node_id: str, code: str, drop: bool
|
||||
) -> FlowDef | None:
|
||||
"""Bring a function node's ports and settings in line with its header.
|
||||
|
||||
@@ -694,7 +694,7 @@ def _follow_header(
|
||||
or definition.origin
|
||||
):
|
||||
return None
|
||||
followed = follow_header(node, code)
|
||||
followed = follow_header(node, code, drop)
|
||||
if followed == node:
|
||||
return None
|
||||
definition.nodes = [followed if n.id == node_id else n for n in definition.nodes]
|
||||
@@ -716,6 +716,7 @@ async def save_node_source(
|
||||
source: NodeSource,
|
||||
controller: FlowControllerDep,
|
||||
user: CurrentUser,
|
||||
drop: bool = True,
|
||||
) -> Any:
|
||||
"""Save a node's source as an unpublished edit and report whether it loads.
|
||||
|
||||
@@ -730,7 +731,9 @@ async def save_node_source(
|
||||
|
||||
A node with its own code takes its input ports and settings from the
|
||||
header of ``process`` (see `follow_header`), and the answer carries the
|
||||
node when that changed it.
|
||||
node when that changed it. ``drop=false`` is the editor's autosave, which
|
||||
adds and renames from the header but keeps what it no longer names until
|
||||
the editor is left.
|
||||
"""
|
||||
definition = _read_flow(controller, name)
|
||||
ref = _source_ref(definition, node_id)
|
||||
@@ -746,7 +749,7 @@ async def save_node_source(
|
||||
controller.store.write_node_source, name, node_id, source.code, True
|
||||
)
|
||||
followed = await run_in_threadpool(
|
||||
_follow_header, controller, name, node_id, source.code
|
||||
_follow_header, controller, name, node_id, source.code, drop
|
||||
)
|
||||
node_def = next((n for n in definition.nodes if n.id == node_id), None)
|
||||
device = (
|
||||
|
||||
@@ -1837,7 +1837,7 @@ def _signature_gap(
|
||||
)
|
||||
|
||||
|
||||
def follow_header(node: NodeDef, code: str) -> NodeDef:
|
||||
def follow_header(node: NodeDef, code: str, drop: bool = True) -> NodeDef:
|
||||
"""The node with its input ports and settings read off ``def process(...)``.
|
||||
|
||||
The inverse of the canvas's scaffold, which writes the input ports first,
|
||||
@@ -1845,7 +1845,9 @@ def follow_header(node: NodeDef, code: str) -> NodeDef:
|
||||
keeps its kind. A new one is a setting when it has a default, which becomes
|
||||
its value, or when it follows a setting, and an input port otherwise. One
|
||||
name swapped for one other is a rename, so the port keeps its message and
|
||||
the setting its value. A declared name the header no longer has is dropped.
|
||||
the setting its value. A declared name the header no longer has is dropped,
|
||||
unless ``drop`` is false: code saved while it is still being typed keeps
|
||||
those, so a pause mid-edit does not cost a port its message.
|
||||
|
||||
A header this cannot read for certain leaves the node as it is: a syntax
|
||||
error, no top-level ``def process``, a decorator, ``*args``, ``**kwargs``,
|
||||
@@ -1907,6 +1909,13 @@ def follow_header(node: NodeDef, code: str) -> NodeDef:
|
||||
after_setting = True
|
||||
else:
|
||||
requires.append(MessageSpec(port=name))
|
||||
if not drop:
|
||||
kept = set(gone) - set(renamed.values())
|
||||
# Where they were, so that typing the name back changes nothing.
|
||||
for index, spec in enumerate(node.requires):
|
||||
if spec.port in kept:
|
||||
requires.insert(index, spec)
|
||||
params |= {key: settings[key] for key in kept if key in settings}
|
||||
# Ports the header cannot name, such as a row not named yet, stay.
|
||||
requires += [spec for spec in node.requires if not written(spec.port)]
|
||||
return node.model_copy(update={"requires": requires, "params": params})
|
||||
|
||||
@@ -580,6 +580,13 @@ def test_saving_a_header_moves_the_node_ports_in_the_draft(
|
||||
assert response["version"] == saved["definition"]["version"] + 1
|
||||
logger = next(n for n in draft["definition"]["nodes"] if n["id"] == "logger")
|
||||
assert logger["params"] == {"gain": 2}
|
||||
# The editor's autosave: a parameter typed away keeps its port for now.
|
||||
paused = client.put(
|
||||
f"{PREFIX}/header/nodes/logger/source?drop=false",
|
||||
headers=superuser_token_headers,
|
||||
json={"code": "def process(reading, gain=2):\n return {}\n"},
|
||||
).json()
|
||||
assert paused["node"] is None
|
||||
|
||||
client.delete(f"{PREFIX}/header", headers=superuser_token_headers)
|
||||
|
||||
|
||||
@@ -192,6 +192,21 @@ def test_a_renamed_parameter_keeps_the_message_and_a_dropped_one_goes():
|
||||
assert dropped.params == {"factor": 3}
|
||||
|
||||
|
||||
def test_without_drop_a_missing_parameter_stays_and_the_rest_follows():
|
||||
node = _flow(
|
||||
[*READING, MessageSpec(name="garden.level", port="depth")], factor=3
|
||||
).nodes[0]
|
||||
|
||||
paused = follow_header(node, "def process(depth): pass\n", drop=False)
|
||||
renamed = follow_header(node, "def process(value, depth, factor): pass\n", False)
|
||||
added = follow_header(node, "def process(depth, gain=2): pass\n", drop=False)
|
||||
|
||||
assert paused == node
|
||||
assert renamed.requires[0] == READING[0].model_copy(update={"port": "value"})
|
||||
assert [spec.port for spec in added.requires] == ["reading", "depth"]
|
||||
assert added.params == {"factor": 3, "gain": 2}
|
||||
|
||||
|
||||
def test_a_new_parameter_after_a_setting_is_a_setting():
|
||||
node = _flow(READING, factor=3).nodes[0]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user